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() ) )
,- The expression
bool( fun(v[0] ) )
is not actually evaluated, because we're in a non-evaluated context (decltype
, similar tosizeof
). 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. void()
isn't really a value, but it behaves like a value in the context of the comma operator anddecltype
.
which I haven'te really understood.
- The expression
(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 thatdecltype(x)
would be routed to (ifx
is an unparenthesized...), pretty much like(
and)
aroundx
. Give this, I'd ask whehterdecltype((unparenthesized-name))
anddecltype(unparenthesized-name, 0)
impose the same requirements onunparenthesized-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 avoid
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 expressionE1
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, butf()
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 viadecltype(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())
?
decltype(std::declval<T>().begin())
rather thandecltype(void(std::declval<T>().begin()))
? – Sherysheryedecltype
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 beint
whereasdecltype(f(), f())
would be the type returned byf()
. – Desmiddecltype
is used to have several expression to test? Well, I'd than assume that when I seedecltype(x,y)
I could change it tostd::pair<decltype(x), decltype(y)>
(for the purpose of testingx
andy
), which doesn't work in general. However, I've clarified why I mentioneddecltype(f(), f())
. Hope that truly clarifies. – Sphagnumdecltype
works? I think the comma doesn't really play into it. Your previous question boils down todecltype
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 makefoo
a sub-expression. – Forellifoo, 0
being well-formed is not necessarily the same asfoo
being well-formed, because,
can mean arbitrary things. Casting tovoid
doesn't suffer that problem,decltype(foo)
->decltype(void(foo))
only adds a completeness requirement. But this is all justdecltype
rules and already quoted. – Forelli