Search code examples
c++c++11castinglanguage-lawyerreinterpret-cast

implicit reinterpret cast on reference without warning/error


Just found the reason for an insidious crash to be a unchecked wild cast by the compiler, disregarding the types. Is this intended behaviour or a compiler bug?

Problem: When a type definition is involved, it is possible to make an implicit reinterpret cast, undermining the type system.

#include <iostream>

template<class A, class B>
inline bool
isSameObject (A const& a, B const& b)
{
  return static_cast<const void*> (&a)
      == static_cast<const void*> (&b);
}


class Wau
  {
    int i = -1;
  };

class Miau
  {
  public:
    uint u = 1;
  };


int
main (int, char**)
{
  Wau wau;
  using ID = Miau &;
  ID wuff = ID(wau);      // <<---disaster

  std::cout << "Miau=" << wuff.u
            << " ref to same object: " <<std::boolalpha<< isSameObject (wau, wuff)
            << std::endl; 
  return 0;
}

I was shocked to find out that gcc-4.9, gcc-6.3 and clang-3.8 accept this code without any error and produce the following output:

Miau=4294967295 ref to same object: true

Please note I use the type constructor syntax ID(wau). I would expect such behaviour on a C-style cast, i.e. (ID)wau. Only when using the new-style curly braces syntax ID{wau} we get the expected error...

~$ g++ -std=c++11 -o aua woot.cpp

woot.cpp: In function ‘int main(int, char**)’:
woot.cpp:31:21: error: no matching function for call to ‘Miau::Miau(<brace-enclosed initializer list>)’
     ID wuff = ID{wau};
                 ^
woot.cpp:10:7: note: candidate: constexpr Miau::Miau()
 class Miau
       ^~~~
woot.cpp:10:7: note:   candidate expects 0 arguments, 1 provided
woot.cpp:10:7: note: candidate: constexpr Miau::Miau(const Miau&)
woot.cpp:10:7: note:   no known conversion for argument 1 from ‘Wau’ to ‘const Miau&’
woot.cpp:10:7: note: candidate: constexpr Miau::Miau(Miau&&)
woot.cpp:10:7: note:   no known conversion for argument 1 from ‘Wau’ to ‘Miau&&’

Unfortunately, the curly-braces syntax is frequently a no-go in template heavy code, due to the std::initializer_list fiasco. So for me this is a serious concern, since the protection by the type system effectively breaks down here.

  • Can someone explain the reasoning behind this behaviour?
  • Is it some kind of backwards compatibility (again, sigh)?

Solution

  • To go full language-lawyer, T(expression) is a conversion of the result of expression to T1. This conversion has as effect to call the class' constructor2. This is why we tend to call a non-explicit constructor taking exactly one argument a conversion constructor.

    using ID = Miau &;
    ID wuff = ID(wau);
    

    This is then equivalent to a cast expression to ID. Since ID is not a class type, a C-style cast occurs.

    Can someone explain the reasoning behind this behaviour?

    I really can't tell why is was ever part of C++. This is not needed. And it is harmful.

    Is it some kind of backwards compatibility (again, sigh)?

    Not necessarily, with C++11 to C++20 we've seen breaking changes. This could be removed some day, but I doubt it would.


    1)

    [expr.type.conv]

    1. A simple-type-specifier or typename-specifier followed by a parenthesized optional expression-list or by a braced-init-list (the initializer) constructs a value of the specified type given the initializer. [...]
    2. If the initializer is a parenthesized single expression, the type conversion expression is equivalent to the corresponding cast expression. [...]

    2) (when T is of class type and such a constructor exists)

    [class.ctor]/2

    A constructor is used to initialize objects of its class type. Because constructors do not have names, they are never found during name lookup; however an explicit type conversion using the functional notation ([expr.type.conv]) will cause a constructor to be called to initialize an object.