11

The output for the code below produces:

void doit(const T1 &, const T2 &) [T1 = unsigned long, T2 = int]
t1 == t2
t1 == (T1)t2
t1 != (T1&)t2
t1 == (T1&&)t2

I understand that the t1 == t2 case is simply an integral promotion.

The second case t1 == (T1)t2 is the same thing, just explicit.

The third case t1 == (T1&)t2 must be a reinterpret_cast of some sort... Though, further explanation would be helpful.

The fourth case t1 == (T1&&)t2 is what I am stuck on. I put in the term 'Temporary Materialization' in the question's title as this is the closest I could come to some sort of answer.

Could someone go over these four cases?

Code:

#include <iostream>    

template <typename T1, typename T2>
void doit(const T1& t1, const T2& t2) {
  std::cout << __PRETTY_FUNCTION__ << '\n';

  if (t1 == t2) {
    std::cout << "t1 == t2" << '\n';
  }
  else {
    std::cout << "t1 != t2" << '\n';
  }    

  if (t1 == (T1)t2) {
    std::cout << "t1 == (T1)t2" << '\n';
  }
  else {
    std::cout << "t1 != (T1)t2" << '\n';
  }    

  if (t1 == (T1&)t2) {
    std::cout << "t1 == (T1&)t2" << '\n';
  }
  else {
    std::cout << "t1 != (T1&)t2" << '\n';
  }    

  if (t1 == (T1&&)t2) {
    std::cout << "t1 == (T1&&)t2" << '\n';
  }
  else {
    std::cout << "t1 != (T1&&)t2" << '\n';
  }
}    

int main() {
  const unsigned long a = 1;
  const int b = 1;    

  doit(a, b);    

  return 0;
}
TylerH
  • 20,799
  • 66
  • 75
  • 101
Supervisor
  • 582
  • 1
  • 6
  • 20
  • 1
    Case4: `if (t1 == static_cast(t2))` creating a temporary of type `T1` from `t2`. Case3 is straight forward UB. – Richard Critten Feb 14 '18 at 22:02
  • How do you know it creates a temporary of type `T1`? Is this in the standard, and if so, could you point where? – Supervisor Feb 14 '18 at 22:07
  • order of casts tried: http://en.cppreference.com/w/cpp/language/explicit_cast and then (1) in http://en.cppreference.com/w/cpp/language/static_cast – Richard Critten Feb 14 '18 at 22:10
  • You could compare also `t1 == static_cast(t2)`. – aschepler Feb 14 '18 at 23:07
  • 2
    This seems unspecified. It sure seems like `const_cast(static_cast(t2))` is a valid interpretation of [this](http://eel.is/c++draft/expr.cast#4.3), but that's not what the compiler is doing, and I'm not sure how it would know how to do that. – Barry Feb 15 '18 at 00:59
  • @ Barry all four variants kind of are in unspecified territory, we don't know length of long (can be equal or long-er than int), format of signed value... I'm not sure what ISO C++ says now about signed to unsigned cast, but C99 states it being dependent of internal representation. – Swift - Friday Pie Feb 15 '18 at 07:16

3 Answers3

2

The compiler attempts to interpret c-style casts as c++-style casts, in the following order (see cppreference for full details):

  1. const_cast
  2. static_cast
  3. static_cast followed by const_cast
  4. reinterpret_cast
  5. reinterpret_cast followed by const_cast

Interpretation of (T1)t2 is pretty straightforward. const_cast fails, but static_cast works, so it's interpreted as static_cast<T1>(t2) (#2 above).

For (T1&)t2, it's impossible to convert an int& to unsigned long& via static_cast. Both const_cast and static_cast fail, so reinterpret_cast is ultimately used, giving reinterpret_cast<T1&>(t2). To be precise, #5 above, since t2 is const: const_cast<T1&>(reinterpret_cast<const T1&>(t2)).

EDIT: The static_cast for (T1&)t2 fails due to a key line in cppreference: "If the cast can be interpreted in more than one way as static_cast followed by a const_cast, it cannot be compiled.". Implicit conversions are involved, and all of the following are valid (I assume the following overloads exist, at a minimum):

  • T1 c1 = t2; const_cast<T1&>(static_cast<const T1&>(c1))
  • const T1& c1 = t2; const_cast<T1&>(static_cast<const T1&>(c1))
  • T1&& c1 = t2; const_cast<T1&>(static_cast<const T1&>(std::move(c1)))

Note that the actual expression, t1 == (T1&)t2, leads to undefined behavior, as Swift pointed out (assuming sizeof(int) != sizeof(unsigned long)). An address that holds an int is being treated (reinterpreted) as holding an unsigned long. Swap the order of definition of a and b in main(), and the result will change to be equal (on x86 systems with gcc). This is the only case that has undefined behavior, due to a bad reinterpret_cast. Other cases are well defined, with results that are platform specific.

For (T1&&)t2, the conversion is from an int (lvalue) to an unsigned long (xvalue). An xvalue is essentially an lvalue that is "moveable;" it is not a reference. The conversion is static_cast<T1&&>(t2) (#2 above). The conversion is equivalent to std::move((T1)t2), or std:move(static_cast<T1>(t2)). When writing code, use std:move(static_cast<T1>(t2)) instead of static_cast<T1&&>(t2), as the intent is much more clear.

This example shows why c++-style casts should be used instead of c-style casts. Code intent is clear with c++-style casts, as the correct cast is explicitly specified by the developer. With c-style casts, the actual cast is selected by the compiler, and may lead to surprising results.

Jay West
  • 1,137
  • 8
  • 5
  • What's the difference from Swift's answer? And again, why does static_cast followed by const_cast not work for `(T1&)t2`? – xskxzr Feb 23 '18 at 03:50
  • Ignoring const for clarity, static_cast does not work for `(T1&)t2` because t2 is treated as `int&`, and there is no valid conversion from `int&` to `unsigned long&`. You are attempting to cast a specific memory address from one type to another, similar to pointer conversion. static_cast allows this only for limited situations where it makes sense to do so (unions and inheritance hierarchies). Swift's answer pointed out the undefined behavior, but didn't explain the underlying type conversion, which was the original question. – Jay West Feb 23 '18 at 16:36
  • 2
    You can't ignore const. There is a valid conversion from `int` to `const unsigned long&`. – xskxzr Feb 24 '18 at 08:55
  • You are correct, xskxzr. Things are very different with and without const. I've added an edit. – Jay West Feb 24 '18 at 23:21
  • The actual wording is https://eel.is/c++draft/expr.cast#4.sentence-4 : "If a conversion can be interpreted in more than one way as a static_­cast followed by a const_­cast, the conversion is ill-formed." So if there were more than one possible route of that form, the compilers should reject the program entirely, not fall back to `reinterpret_cast`. In fact, this is a bug in (pretty much every) compiler, and they should perform static_­cast followed by a const_­cast - there is no `c1` anywhere so I don't agree that there's more than one possible route. – ecatmur Mar 02 '22 at 17:03
2

Let's look at (T1&&)t2 first. This is indeed a temporary materialization; what happens is that the compiler performs lvalue-to-rvalue conversion on t2 (i.e. accesses its value), casts that value to T1, constructs a temporary of type T1 (with value 1, since that is the value of b and is a valid value of type T1), and binds it to an rvalue reference. Then, in the comparison t1 == (T1&&)t2, both sides are again subject to lvalue-to-rvalue conversion, and since this is valid (both refer to an object of type T1 within its lifetime, the left hand side to a and the right hand side to the temporary) and both sides have value 1, they compare equal.

Note that a materialized temporary of type T1 (say) can bind either to a reference T1&& or T1 const&, so you could try the latter as well in your program.

Also note that while the T1 converted from t2 is a temporary, it would be lifetime extended if you bound it to a local variable (e.g. T1&& r2 = (T1&&)t2;). That would extend the lifetime of the temporary to that of the local reference variable, i.e. to the end of the scope. This is important when considering the "with its lifetime" rule, but here the temporary is destroyed at the end of the expression, which is still after it is accessed by the == comparison.

Next, (T1&)t2 should be interpreted as a static_cast reference binding to a temporary T1 followed by a const_cast; that is, const_cast<T1&>(static_cast<T1 const&>(t2)). The first (inner) cast materializes a temporary T1 with value converted from t2 and binds it to a T1 const& reference, and the second (outer) cast casts away const. Then, the == comparison performs lvalue-to-rvalue conversion on t1 and on the T1& reference; both of these are valid since both refer to an object of type T1 within its lifetime, and since both have value 1 they should compare equal. (Interestingly, the materialized temporary is also a candidate for lifetime extension, but that doesn't matter here.)

However, all major compilers currently fail to spot that they should do the above (Why is (int&)0 ill-formed? Why does this C-style cast not consider static_cast followed by const_cast?) and instead perform a reinterpret_cast (actually a reinterpret_cast to T1 const& followed by a const_cast to T1&). This has undefined behavior (since unsigned long and int are distinct types that are not related by signedness and are not types that can access raw memory), and on platforms where they are different sizes (e.g. Linux) will result in reading stack garbage after b and thus usually print that they are unequal. On Windows, where unsigned long and int are the same size, it will print that they are equal for the wrong reason, which will nevertheless be undefined behavior.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
-2

Technically all four variants are platform dependent

t1 == t2

t2 gets promoted to 'long', the casted to unsigned long.

t1 == (T1)t2

signed int gets converted to its representation as unsigned long, in order being casted to long first. There were debates if this must be considered an UB or not, because result is depending on platform, I honestly not sure how it is defined now, aside from clause in C99 standard. Result of comparison would be same as t1 == t2.

t1 == (T1&)t2

The cast result is a reference, you compare reference ( essentially a pointer) to T1, which operation yields unknown result. Why? Reference js pointing to a different type.

t1 == (T1&&)t2

The cast expression yields an rvalue, so you do compare values, but its type is rvalue reference. Result of comparison would be same as t1 == t2.

PS. Funny thing happen (depends on platform, essentially an UB) if if you use such values:

  const unsigned long a = 0xFFFFFFFFFFFFFFFF;
  const int b = -1;  

Output might be:

t1 == t2
t1 == (T1)t2
t1 == (T1&)t2  
t1 == (T1&&)t2

This is one of reasons why you should be careful in comparing unsigned to signed values, especially with < or >. You'll be surprised by -1 being greater than 1.

if unsigned long is 64bit int and pointer is equal to 64-bit int. -1 if casted to unsigned, becomes its binary representation. Essentially b would become a pointer with address 0xFFFFFFFFFFFFFFFF.

Swift - Friday Pie
  • 12,777
  • 2
  • 19
  • 42
  • Comparing reference has no difference from comparing the object it refers to. – xskxzr Feb 15 '18 at 09:31
  • Does not answer the question in any way. – SergeyA Feb 15 '18 at 14:24
  • @xskxzr t2 is int with value 1, t1 is long and (T1&)t2 is a reference to storage of size of long with same address as t2. If long is different from int, you'll have undefined behaviour, it is an another way of type punning. My example intentionally arranged to have cance to get same result in all four cases, including reference reading part of a. On some platforms long is identical to int, so you'll have same result. – Swift - Friday Pie Feb 15 '18 at 15:30
  • @Swift Why does the reference refer to original `t1` object? As Barry pointed out in the [comment](https://stackoverflow.com/questions/48796854/c-r-value-reference-casting-and-temporary-materialization/48801533?noredirect=1#comment84599909_48796854), the reference should refer to a temporary of type `unsigned long`. – xskxzr Feb 16 '18 at 03:04
  • @xskxzr none of compilers I tried do not generate same code in this case. gcc\clang would refer to original object, a temporary like you offer is created by vc2010-vc2015 with part of the `long long` (instead of `long` to make it match wider int) uninitialized , intel would initialize that temporary properly. `static_cast(t2)` for gcc is invalid, and it doesn't do Barry's const trick, so it would do `reinterpret_cast`?. vc doesn't do `const_cast(static_cast(t2))` exactly either, generated code is something weird. It must be a defect though. – Swift - Friday Pie Feb 16 '18 at 03:51