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.
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]
- 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. [...]
- 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]/2A 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.
it is possible to make an implicit reinterpret cast, undermining the type system.
ID wuff = ID(wau);
That's not an "implicit" reinterpret cast. That is an explicit type conversion. Although, the fact that the conversion does reinterpretation is indeed not easy to see. Specifically, the syntax of the cast is called "functional style".
If you're unsure what type of cast an explicit type conversion (whether using the functional syntax, or the C style syntax) performs, then you should refrain from using it. Many would argue that explicit type conversions should never be used.
If you had used static_cast instead, you would have stayed within the protection of the type system:
ID wuff = static_cast<ID>(wau);
error: non-const lvalue reference to type 'Miau' cannot bind to a value of unrelated type 'Wau'
It's often also safe to simply rely on implicit conversions:
ID wuff = wau;
error: non-const lvalue reference to type 'Miau' cannot bind to a value of unrelated type 'Wau'
Is this intended behaviour
Yes.
or a compiler bug?
No.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With