The full story about the decltype + comma trick
Asked Answered
S

1

6

The title should clarify what my confusion is about. I'd like to use this question to get a comprehensive answer which helps me understand how the comma operator works with decltype within SFINAE context. An example usage is shown in this question of mine.

A bit of research

Searching for is:question [c++] [decltype] comma, one finds these 6 questions:

  • (2011) How is decltype supposed to work with operator, has a promising title, but it is VisualStudio specific and the one and only answer fundamentally claims there's a bug in the compiler indeed.

  • (2012) How is type deduced from auto return type? The accepted answer says the following, with reference to the expression decltype( ( bool( fun(v[0] ) ), void() ) ),

    1. The expression bool( fun(v[0] ) ) is not actually evaluated, because we're in a non-evaluated context (decltype, similar to sizeof). What matters here is that it would be evaluated if the expression as a whole was evaluated, so that if the subexpression is invalid then the whole expression is invalid.
    2. void() isn't really a value, but it behaves like a value in the context of the comma operator and decltype.

    which I haven'te really understood.

  • (2014) c++11 decltype returns reference type doesn't go into much detail, but the accepted answer expresses the idea that i, j is an lvalue expression and, as such, decltype returns a reference type for it, so if I correctly understand it essentially says , 1 can be a tool to disable the special case that decltype(x) would be routed to (if x is an unparenthesized...), pretty much like ( and ) around x. Give this, I'd ask whehter decltype((unparenthesized-name)) and decltype(unparenthesized-name, 0) impose the same requirements on unparenthesized-name.

  • (2015) Why cannot we use brace initializer in an un-evaluated context? together with its in-body question why cannot we use void{} instead? seems a bit unrelated, as it's mostly about whether one can create a void value or not.

  • (2015) SFINAE: static_cast<void>() or , void()? There's no accepted answer, so if the asker is not happy with any, I trust their rep and badges and I'm not happy with those answers either.

  • (2020) How to use sfinae to exclude types for which a function is defined? seems unrelated.

But I haven't understood the full story yet.

My understanding

From cppreference I read that

In a comma expression E1, E2, the expression E1 is evaluated, its result is discarded ([…]), and its side effects are completed before evaluation of the expression E2 begins ([…]).

The type, value, and value category of the result of the comma expression are exactly the type, value, and value category of the second operand, E2.

so I'm tempted to understand that as far as the code in which E1, E2 appears is concerned, the only thing that makes the E1, E2 expression different from just E2 is the side effects of E1.

However the page obviously doesn't adderess the use of operator, within decltype. So let's see what decltype's doc page has to say:

The type need not be complete or have an available destructor, and can be abstract. This rule doesn't apply to sub-expressions: in decltype(f(g())), g() must have a complete type, but f() need not.

(Correct me if I'm wrong, but I guess this sentence applies to the whole point (2), not just point (2.c). Also, I assume that the last f() is referring to the expression f(g()).)

In a comment under this question of mine, @cigien comments that passing f(g()) or f(), 0 to decltype enforces the same requirement of completeness on the return type of f.

Based on that, I deduce that decltype(f(), 0) is just a way to have something to put in std::void_t<> in a SFINAE context when we want to

  • make sure f() is well formed, just as we'd do via decltype(f()),
  • but also requiring that the return type be complete, which wouldn't be enforced by decltype(f()).

The , 0, I understand, is just a trick we use to have f() be the first argument to ,. We could just as well do decltype(f(), f()), and would enforce the same requirement on f; yes, decltype would result in the return type of f rathern than int, but that's irrelevant if we are feeding that type to std::void_t<>.

As regards the whole comma trick, however, HolyBlackCat says it is not robust

So if begin() is valid and returns a complete type, but , is overloaded and can't be called for some reason, you'll get a false negative

(my demo of the statement above) and proposes decltype(void(std::declval<T>().begin())) as a better alternative. But how does this work? Why converting to void triggers something more than not converting it, i.e. decltype(std::declval<T>().begin())?

Sphagnum answered 28/9, 2021 at 18:11 Comment(7)
"...than not doing it" is this a typo? decltype(std::declval<T>().begin()) rather than decltype(void(std::declval<T>().begin())) ?Sherysherye
@463035818_is_not_a_number, I'm not sure. I've worded it differently. Is it ok now?Sphagnum
Comma in decltype is used to have several expression to test (decltype(c.begin(), c.end(), 0)) and mostly to select the wanted return type. decltype(f(), 0) would be int whereas decltype(f(), f()) would be the type returned by f().Desmid
@Jarod42, I know that. Comma in decltype is used to have several expression to test? Well, I'd than assume that when I see decltype(x,y) I could change it to std::pair<decltype(x), decltype(y)> (for the purpose of testing x and y), which doesn't work in general. However, I've clarified why I mentioned decltype(f(), f()). Hope that truly clarifies.Sphagnum
I'm not actually sure what the question is here. Is this just about how decltype works? I think the comma doesn't really play into it. Your previous question boils down to decltype of a prvalue can yield an incomplete or abstract type, but that doesn't waive those requirements for sub-expressions. decltype(foo) -> decltype(foo, 0) is one way to make foo a sub-expression.Forelli
HolyBlackCat's answer alluded to the fact that this is subject to comma overloading. That is, foo, 0 being well-formed is not necessarily the same as foo being well-formed, because , can mean arbitrary things. Casting to void doesn't suffer that problem, decltype(foo) -> decltype(void(foo)) only adds a completeness requirement. But this is all just decltype rules and already quoted.Forelli
You need to add a paragraph at the beginning to explain what the "trick" is for.Schreibman
R
0

It is generally required that prvalues have complete type ([basic.lval]/9), and the exception to that for decltype applies only to the expression as a whole ([dcl.type.decltype]/2). Casting an expression to void therefore imposes a completeness requirement on its type (if no reference is involved); the choice of that particular manipulation is driven by its genericity (anything can be cast to void) and by the known resulting type. This result can be directly matched against a class=void template parameter introduced to support such SFINAE, or can have , applied (which would also require completeness) without fear of overloading in order to select some other fixed type for the overall expression.

Reo answered 20/5, 2023 at 4:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.