Custom static cast function template in C++14

194 views Asked by At

I've written such a template function for static casting as kind of an exercise:

template<typename cT, typename T>
inline constexpr cT sCast(T carg) {
    return static_cast<cT>(carg);
}

I just didn't want to type static_cast<someT>(some_obj) all the time, so I challenged myself to write my own template function with a shorter name. However, I knew I can do something much easier in one line of code:

#define sCast static_cast
   ...
sCast<someT>(some_obj);

But as I said, I wanted to design a function template just for exercise.

Now my question is: is this function template as efficient as possible and if not, how can it be better? Will template functions meet conditions of RVO optimalization? Or maybe I'm just a perfectionist and such ideas are waste of time?

3

There are 3 answers

0
HelpfulHelper On BEST ANSWER

this template is not as efficient as just using static_cast. The reason is that the function has to be explicitly pushed onto the stack, and its arguments have to be cleaned up afterwards. Most compilers will inline it (==> it is as if you wrote static_cast instead of the function from above), but if your compiler chooses to not inline it, it will impact your performance (even though just slightly, and its unlikely anyone would ever notice that)

0
fabian On

Your template is not a replacement for static_cast at all.

Consider the case where references are involved. Without specifying the second parameter explicitly, you'll end up without references being deduced:

struct Test
{
    Test()
    {
        std::cout << "Test::Test()\n";
    }

    Test(Test&&)
    {
        std::cout << "Test::Test(Test&&)\n";
    }

    Test(Test const&)
    {
        std::cout << "Test::Test(Test const&)\n";
    }

    Test& operator=(Test&&)
    {
        std::cout << "Test::operator=(Test&&)\n";
        return *this;
    }

    Test& operator=(Test const&)
    {
        std::cout << "Test::operator=(Test const&)\n";
        return *this;
    }

    ~Test()
    {
        std::cout << "Test::~Test()\n";
    }
};

template<typename cT, typename T>
inline constexpr cT sCast(T carg) {
    return static_cast<cT>(carg);
}

int main()
{
    Test original;
    std::cout << "before static_cast\n";
    Test const& withStaticCast = static_cast<Test const&>(original);
    std::cout << "before sCast\n";
    Test const& withScast = sCast<Test const&>(original);
    std::cout << "after sCast\n";

    std::cout << std::boolalpha
        << (&original == &withStaticCast) << '\n'
        << (&original == &withScast) << '\n';
}

Output:

Test::Test()
before static_cast
before sCast
Test::Test(Test const&)
Test::~Test()
after sCast
true
false
Test::~Test()

As you can see there's an extra copy being made by sCast and this copy is even destroyed before the next expression which could easily result in undefined behaviour.

Using forwarding references you could fix this problem, but you'd still end up with different semantics in some cases:

template<typename cT, typename T>
constexpr cT sCast(T&& carg)
{
    return static_cast<cT>(std::forward<T>(carg));
}

Code where this has different semantics:

std::cout << "before static_cast\n";
Test withStaticCast = static_cast<Test>(Test());
std::cout << "before sCast\n";
Test withScast = sCast<Test>(Test{});
std::cout << "after sCast\n";

Output

before static_cast
Test::Test()
before sCast
Test::Test()
Test::Test(Test&&)
Test::~Test()
after sCast

My recommendation is to keep using static_cast. This will also help other people reading your code; they are almost certainly familiar with static_cast, but it's likely they'll need to look up sCast, at least the first time they encounter that function. Also IDEs will highlight static_cast differently than function template calls.

In general I don't consider it a good idea to shorten the code at all cost. If by typing a few more chars you add readability, it's worth the effort. If you cannot be bothered with the typing, there's still search&replace functionality available in pretty much every text editor...

0
Artyer On

In C++14: casting to a non-reference type will create a copy. That copy is subject to RVO, but static_cast will just never have that extra temporary to copy/move from. For example:

struct X { operator std::string(); operator std::mutex(); };

// Same as `X{}.operator std::string();`
static_cast<std::string>(X{});

// Same as `std::string(X{}.operator std::string())`: May move, may be elided
sCast<std::string>(X{});

static_cast<std::mutex>(X{});  // Same as `X{}.operator std::mutex();`
//sCast<std::mutex>(X{});  // Does not compile: mutex not movable

This problem does not exist in C++17 since the elision of that move is mandatory.


There is also special wording for static_cast. Lifetime extension can be done through static casts and they are not type-dependent if the type casted to isn't.

const X& x = static_cast<const X&>(X{});  // This will extend the lifetime of the temporary
const X& y = sCast<const X&>(X{});  // This will be a dangling reference

struct TempFn { template<typename> void f(); };
template<typename T>
void g(T t) {
    // OK: static_cast<TempFn>(t) is not type-dependent
    static_cast<TempFn>(t).f<int>();
    // Error: sCast<TempFn>(t) is type-dependent
    //sCast<TempFn>(t).f<int>();
    sCast<TempFn>(t).template f<int>();
}

This cannot be emulated with a function template.


Your function also doesn't handle rvalues properly and always copies it's arguments. E.g.:

struct Y : X { operator int() &&; };

static_cast<int>(Y{});  // Works
sCast<int>(Y{});  // Tries to cast lvalue `carg` to int. Does not work.

Y y;
static_cast<X&>(y);  // Works: reference to base of y
sCast<X&>(y);  // Dangling reference to base of temporary

This can be fixed with perfect forwarding:

template<typename cT, typename T>
inline constexpr cT sCast(T&& carg) {
    return static_cast<cT>(std::forward<T>(carg));
}

Your function isn't SFINAE friendly. During overload resolution, it will claim to always be callable. For example:

template<typename Integral>
std::enable_if_t<std::is_integral_v<Integral>> f(Integral x);

template<typename Stringy>
decltype(void(sCast<std::string>(std::declval<Stringy>()))) f(Stringy&& s) {
    std::string str = sCast<std::string>(std::forward<Stringy>(s));
    return f(parseint(str));
}

// sCast<std::string>(std::declval<Stringy>()) isn't a substitution failure
// even if Stringy is `int`

You can fix this by making it a substitution failure when the static_cast would fail. Easiest is like this:

template<typename cT, typename T>
inline constexpr decltype(static_cast<cT>(std::declval<T>())) sCast(T&& carg) {
    return static_cast<cT>(std::forward<T>(carg));
}

Your function template cannot take prvalues properly. The "perfect forwarding" above changes prvalues into xvalues. For example:

// The same as `X{}`.
static_cast<X>(X{});

// Cannot be elided. Required to move from a temporary, and for `X` to be move constructible.
// Possibly moves from two temporaries as described in the first point in C++14
sCast<X>(X{});

// C++23
// operator int() can only be called on a prvalue
struct Z { Z() = default; Z(Z&&) = delete; operator int(this Z); };
static_cast<int>(Z{});  // Works
sCast<int>(Z{});  // Tries to call operator int on an xvalue. Cannot move into object parameter.