6

Here's the implementation of std::is_assignable, I have spent few hours trying to get how it can statically figure out the type of the templated object but couldn't.

In the standard is_assignable states that both sides of the assignment are converted to std::add_rvalue_reference<T>::type. I don't get that sentence nor see how std::add_rvalue_reference<T>::typecan be used to predict the type of the object.

Would anyone be able to give me a simple explanation that I can use as a first step to understand how std::is_assignable works?

Kam
  • 5,878
  • 10
  • 53
  • 97

1 Answers1

7

Here's the same code with some of the lexical obfuscation taken out:

 1043     template <typename Tp, typename Up>
 1044     class is_assignable_helper
 1046     {
 1047       template <typename Tp1, typename Up1>
 1048         static decltype(declval<Tp1>() = declval<Up1>(), one())
 1049         test(int);
 1050 
 1051       template<typename, typename>
 1052         static two test(...);
 1053 
 1054     public:
 1055       static constexpr bool value = sizeof(test<Tp, Up>(0)) == 1;
 1056     };

This class uses SFINAE to do the dirty work. That is, the value of variable value will depend on which test() function is chosen based on overload resolution. One overload takes an integer and the other takes a C variadic argument (specified by the ellipsis). If there is a substitution failure that occurs on the first overload, the second overload will be chosen.

If a substitution failure does occur, it will have come from the expression declval<Tp1>() = declval<Up1>(). declval<T>() is a function declaration that "returns" a value of the type std::add_rvalue_reference<T>::type. This function is mainly used in unevaluated contexts like decltype(), sizeof(), noexcept(), etc, in order to get an instance of a type without explicitly calling a constructor (because that type may not have an accessible constructor). If you'd like to know why add_rvalue_reference is the chosen return type, see this post.

Once you get an instance of the type, you can call member/non-member functions on those instances. The member function used is operator=(). If a class doesn't have an assignment operator (or has an inaccessible one) there will be a substitution failure. The fallback (variadic) version of test() will instead be chosen.

The reason for the difference in argument types (int vs ...) is because ... has the lowest conversion rank and it acts as a "last resort" for overload resolution. We can't just leave the parameters empty or else we'll get a redeclaration error.

As for the return type of test - if a substitution failure does not occur (a value of type Up can be assigned to a value of type Tp) then test() returns a type indicating success. If a substitution failure does happen, the the fallback version that returns a type indicating failure is chosen. These types are differentiated through a check for their sizes. We check for success by comparing with 1.

Community
  • 1
  • 1
David G
  • 94,763
  • 41
  • 167
  • 253
  • `==` is not the assignment operator. – Oktalist Sep 29 '14 at 01:15
  • *"in order to get an instance of a type without explicitly calling a constructor"* or destructor. -- `operator=()` needs a parameter – dyp Sep 29 '14 at 01:34
  • Ok I will need a bit of time to understand the answer, as few of the terms and methods used are not familiar to me. But thanks a lot – Kam Sep 29 '14 at 01:36
  • @dyp Not sure I understand the comment. Was that sentence inaccurate? – David G Sep 29 '14 at 01:41
  • I don't get this sentence: 'template static two test(...);' what is 'two' – Kam Sep 29 '14 at 01:46
  • @Kam `one` and `two` are typedefs such that `sizeof(one)` is 1 and `sizeof(two)` is 2. – T.C. Sep 29 '14 at 01:49
  • @Kam In the original code `__is_assignable_helper` derived from `__sfinae_types` which most likely provided the `one` and `two` types (probably `char` and `char(&)[2]` respectively). Their sizes are what is important because they differentiate between success and failure. – David G Sep 29 '14 at 01:50
  • You have been very helpul, thank you for the above, but I still don't get from your explanation how decltype(declval() = declval(), one()) can figure out that Up1 is Assignable to Tp1 can you elaborate a bit more in the explanation of that part if you can? – Kam Sep 29 '14 at 01:53
  • 3
    `declval() = declval()` isn't actually assigning anything. Remember that `decltype` is an unevaluated context meaning the only thing it cares about are types, not values. If the assignment operator is not overloaded for those two types (or one doesn't exist or it's private, etc.) then a substitution failure will occur. When that happens that specific overload of `test()` is removed as a viable function for those two types, the second (last resort) overload is instead taken. – David G Sep 29 '14 at 01:59
  • `declval` returns an rvalue-reference and not a prvalue to not require an accessible destructor. `operator=` needs a parameter, so it should be `operator=(A)`, but we do not know `A` (since there could be conversions). Both are minor remarks, you've already got +1 from me. – dyp Sep 29 '14 at 12:34