7

I came across with this code in an std::optional implementation :

template <class T, class U>
struct is_assignable
{
  template <class X, class Y>
  constexpr static bool has_assign(...) { return false; }

  template <class X, class Y, size_t S = sizeof((std::declval<X>() = std::declval<Y>(), true)) >
  // the comma operator is necessary for the cases where operator= returns void
  constexpr static bool has_assign(bool) { return true; }

  constexpr static bool value = has_assign<T, U>(true);
};

The part that I cant understand how it works or how it is evaluated is size_t S = sizeof((std::declval<X>() = std::declval<Y>(), true)) I know that if the assign operation fails it will fall back to the first definition of has_assign that returns false, but i don't know why it has the , true) part.

I did some test with structs that returns void on the assign operator and removing the , true part in sizeof gives me the same results.

alter
  • 333
  • 4
  • 14
  • 2
    http://en.cppreference.com/w/cpp/language/sfinae . If the expression inside `sizeof` is valid for given `X` and `Y`, then there are two overloads of `has_assign`, and the second one is chosen for `has_assign(true)` as a better match. If the expression is meaningless, then that overload is discarded, and the first one is chosen. – Igor Tandetnik Oct 28 '16 at 14:22
  • 1
    Some compilers define `sizeof(void) == 1` as an extension. Enable more warnings. – Quentin Oct 28 '16 at 14:23
  • 2
    Would be more direct to just write `class S = decltype(std::declval() = std::declval())`. We don't care what `S` is anyway, just that that expression is valid. – Barry Oct 28 '16 at 14:24
  • 3
    @SamVashavchik The way I understand this question, it's *not* about SFINAE. The OP even says "I know that if the assign operation fails it will fall back to the first definition of has_assign that returns false..." The real question is why the comma operator (`, true`) is used, which has little to do with SFINAE itself. – Angew is no longer proud of SO Oct 28 '16 at 14:25
  • @Quentin The code in question does not pass an expression of type `void` to `sizeof`. The relevance of your comment to the issue at hand is unclear to me. – Igor Tandetnik Oct 28 '16 at 14:25
  • @IgorTandetnik: it absolutely does if `X::operator=(Y&&)` returns `void`. –  Oct 28 '16 at 14:26
  • 1
    @Fanael Does not. Look closer at what's inside `sizeof`. It's `sizeof(something=something, true)`. I suppose it could be made to break if I make `X::operator=(Y)` return a class that overloads `operator,()` – Igor Tandetnik Oct 28 '16 at 14:27
  • @IgorTandetnik I'm commenting on OP's last sentence, where "[...] removing the `, true` part in `sizeof` gives [her] the same results". – Quentin Oct 28 '16 at 14:28
  • @IgorTandetnik: read the question again, especially this part: "I did some test with structs that returns void on the assign operator and removing the `, true` part in `sizeof` gives me the same results." –  Oct 28 '16 at 14:28
  • @Fanael I see. I have misunderstood the thrust of the question. Never mind me, then. – Igor Tandetnik Oct 28 '16 at 14:30

2 Answers2

8

In principle, the type of the expression std::declval<X>() = std::declval<Y>() (that is, the return type of the operator = involved) can be arbitrary, including an incomplete type or void. In such case, SFINAE wouldn't kick in, since the expression is valid. However, you'd then get an error from applying sizeof to an incomplete type. (Note that some compilers define sizeof(void) == 1 as an extension, but that cannot be universally relied upon).

Adding , true after the SFINAE expression fixes this by discarding the type of the assignment (whatever it is), and applying sizeof to true instead (which is perfectly valid).

As indicated by Barry in the comments, a more direct approach would be to use the type of the assignment in decltype instead of in sizeof, like this:

template <class X, class Y, class S = decltype(std::declval<X>() = std::declval<Y>()) >
constexpr static bool has_assign(bool) { return true; }
Community
  • 1
  • 1
Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
8

In order to apply sizeof(), you need a complete type. But returning a complete type isn't a requirement of assignability, hence:

sizeof((std::declval<X>() = std::declval<Y>(), true))
       ~~~~~~~~~~~~~~~~~~ expr ~~~~~~~~~~~~~~~~~~~~~

if the assignment is valid for those two types, then we have sizeof(expr) where the type of expr is bool (because true). So if the assignment is valid, we get some real size. Otherwise, substitution failure.


But this is an unnecessarily cryptic way of writing this code. Moreover, it's not even correct because I could write a type like:

struct Evil {
    template <class T> Evil operator=(T&& ); // assignable from anything
    void operator,(bool);                    // mwahahaha
};

and now your sizeof() still doesn't work.

Instead, prefer simply:

class = decltype(std::declval<X>() = std::declval<Y>())

This accomplishes the same result - either substitution failure or not - without needed to care at all about what the type of the result is or to handle special cases.

Quentin
  • 62,093
  • 7
  • 131
  • 191
Barry
  • 286,269
  • 29
  • 621
  • 977
  • Would the original code works on some compilers (maybe older and not fully complaint with the standard)? One possible advantage might be in generated code. Since `sizeof(true)` would always give the same size (assuming all files compiled with compatible options), then it would help ensure that only one function is kept if inlining is not done... – Phil1970 Oct 28 '16 at 14:54
  • @Phil1970 This is all done at compile time. None of these functions should appear in the object file. – Barry Oct 28 '16 at 15:02
  • Just a note, there are some cases where is_assignable (even the standard one) can give you a false positive. But I think a new question could be posted on the matter. [gist](https://gist.github.com/aindigo/69bf0ca558a127321af70dbb82314f5c) – alter Oct 28 '16 at 15:15
  • 1
    @alter That's because `Ex1::operator=` is lying about what types it's assignable from. That's not a `std::is_assignable` problem. – Barry Oct 28 '16 at 15:18