When an int is not an int

The more I learn about C++ the more I understand that it is considered a difficult language to master. It has all these quirks and things that are far from obvious which may lead to unforeseen behavior. As an example, look at the following code:

template <typename T>
void func(T&& t) {
  if (std::is_same<T, int>::value) {
    std::cout << t << " is an int!\n";
  } else {
    std::cout << t << " is not an int!\n";
  }
}

int main() {
  int one = 1;
  func(one);
}

From the headline of this post and the introductory text you probably guessed that the output when running this program is 1 is not an int!

But what happens when I change the calling code to this:

int main() {
  func(1);
}

The program will now output 1 is an int! What’s going on here? How can 1 be an integer in one case but not in the other? To understand this we need to learn two things, a little bit about how type deduction works and which qualifications in a type that std::is_same takes into account.

Let’s start with the type deduction. You might think that T&& in the code above denotes an rvalue reference, however this is not true in this case. If the function was an ordinary function and not a function template this would be true. However, for function templates, a T&& denotes a Forwarding Reference, which can be either an rvalue reference or an lvalue reference. Now, in the first case where we have a variable named one of type int with the value 1, one is an lvalue. In order for the function func to be able to accept one as an argument it must accept an lvalue reference. The code after type deduction looks like this:

void func(int& t) {
  if (std::is_same<int&, int>::value) {
    std::cout << t << " is an int!\n";
  } else {
    std::cout << t << " is not an int!\n";
  }
}

Note that both T (used in std::is_same) and T&& (used as the parameter type) is deduced to int&. Now it should be obvious that std::is_same<int&, int>::value is false, since int is not the same as int&.

What about the second case? In that the argument to func is an rvalue so func will take an rvalue reference as argument:

void func(int&& t) {
  if (std::is_same<int, int>::value) {
    std::cout << t << " is an int!\n";
  } else {
    std::cout << t << " is not an int!";
  }
}

In this case T is deduced to int and T&& to int&&. This is the way forwarding references work, they can deduce to either rvalue or lvalue references. By deducing T to an int we get a function that takes an rvalue reference as parameter (just replace T with int and we get func(int&& t)). This is why std::is_same<int, int>::value is true in this case.

The rules also states that T&&& becomes T&, so by deducing T to int& in the first case, void func(T&&) becomes void func(T& &&) which becomes void func(T&) and we get a function that accepts and lvalue reference.

I understand that this is mind boggling and sometimes C++ is far from intuitive, but stick in there!

Happy coding!

Lämna en kommentar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *