Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Surprising c-style cast

Tags:

c++

casting

I am refactoring our code-base, where I have the following code (simplified):

template <typename T>
class TVector3;

template <typename T>
class TVector4;
  
template <typename T>
struct TVector4
{
    TVector3<T>& V3()                   {   return (TVector3<T> &) *this;                       }
    const TVector3<T>& V3() const       {   return (const TVector3<T> &) *this;                 }
};
   
template <typename T>
struct TVector3
{
    template <typename U>
    constexpr TVector3(const TVector4<U>& v) noexcept { }
};

typedef TVector3<float> Vec3f;
typedef TVector4<float> Vec4f;

struct RGBA
{
    Vec4f rgba;
    operator Vec3f() const              {   return rgba.V3();       }
};

clang warns me about returning reference to local temporary object (https://godbolt.org/z/ccxbjv771). Apparently (const TVector3<T> &) *this results in calling TVector3(const TVector4<U>& ), but why ?

Intuitively, I would have expected (const TVector3<T> &) to behave like reinterpret cast, or if at least the cast would have looked like (const TVector3<T>) *this (without &) it would kind of make sense to me that the compiler picked that constructor call for conversion.

Another simpler example:

#include <iostream>

struct A { };

struct B
{
    B(const A& ) { std::cout << "B(const A&)" << std::endl; }
};

int main()
{
    A a;
    (const B&) a;
    return 0;
}

It prints B(const A&) (https://godbolt.org/z/ocWjh1eb3), but why ? I am converting to const B& and not to B.

like image 869
jcxz Avatar asked Dec 22 '22 15:12

jcxz


1 Answers

It calls the constructor because such are the rules of c-style cast:

cppreference: When the C-style cast expression is encountered, the compiler attempts to interpret it as the following cast expressions, in this order:

  • a) const_cast<new_type>(expression);
  • b) static_cast<new_type>(expression), with extensions: pointer or reference to a derived class is additionally allowed to be cast to pointer or reference to unambiguous base class (and vice versa) even if the base class is inaccessible (that is, this cast ignores the private inheritance specifier). Same applies to casting pointer to member to pointer to member of unambiguous non-virtual base;
  • c) static_cast (with extensions) followed by const_cast;
  • d) reinterpret_cast<new_type>(expression);
  • e) reinterpret_cast followed by const_cast. The first choice that satisfies the requirements of the respective cast operator is selected, even if it cannot be compiled (see example). If the cast can be interpreted in more than one way as static_cast followed by a const_cast, it cannot be compiled.

static_cast is chosen because it considers the constructors. Please do not use c-style cast, you see the rules are not so easy and it forced you to make a question about them.

Intuitively, I would have expected (const TVector3 &) to behave like reinterpret cast

You wouldn't want that, it would break strict aliasing. But if you remove the constructor, it will happily do that as per d).

#include <iostream>

struct A { };

struct B
{
    B(const A& ) { std::cout << "B(const A&)" << std::endl; }
};
struct C
{
};

int main()
{
    A a;
    const B& temp1 = (B&) a;// Ctor, copy, prolonged-life good.
    const B& temp2 = (const B&) a;// Ctor, copy, prolonged-life good.
    B& dangling_temp = (B&) a;// Ctor, copy, no prolongment->dangling ref, BAD.
    (const C&) a;// REINTERPET_CAST
    //(const C) a;// Compiler error, good.
    (const C*) &a;// REINTERPET_CAST
    return 0;
}

But a is not a B? If you really really want it to be B, use reinterpret_cast(but don't) or bit_cast explicitly. The sane thing is to attempt to make a copy if possible. It creates a new temporary B and binds it to const B&. Had you stored it into const B& b, it would prolong the temporary's lifetime, making the code safe.

like image 139
Quimby Avatar answered Jan 01 '23 21:01

Quimby