Comma operator in constant-expression

195 views
Skip to first unread message

David Krauss

unread,
Sep 21, 2013, 10:39:07 PM9/21/13
to std-dis...@isocpp.org
C++98 forbade the comma operator in a constant-expression context. C++11 makes a constant-expression a grammatical conditional-expression, which excludes the comma at the top level but allows it inside parentheses.

switch( i ) {
   
case (3,2): std::cout << "i is two\n";
}

It's fairly useless, but can be used for SFINAE where even discarded expressions are significant.

template< typename t >
auto fn( t o )
-> typename std::enable_if< ( std::declval< t >() * std::declval< t >(), true ) >::type
   
{ std::cout << o * o << '\n'; }

Note that declval or void must be used because the expression is evaluated, although nothing could potentially come of it.

template< typename t >
auto fn( t o )
-> typename std::enable_if< ( o * o, true ) >::type // Error (SFINAE?)
   
{ std::cout << o * o << '\n'; }

There doesn't seem to be implementation consensus. Clang accepts all such examples but GCC complains that the expression is "used," even if protected by a cast to void or decltype. SFINAE is not applied so any call to the overload set stops compilation. (Previous versions of GCC ignored the LHS of the comma under SFINAE, as well as unevaluated RHS of && and ||.)

David Krauss

unread,
Sep 22, 2013, 3:23:23 AM9/22/13
to std-dis...@isocpp.org
I hadn't experimented enough before the last post. The only difference between GCC and Clang is whether the error is swallowed by SFINAE, which is down to UB from a template that cannot produce any well-formed instantiation.

I was wrong to suppose that void() or declval might make a difference, since void() just produces a discarded-value expression, which the LHS of comma already is, and declval merely has restricted uses; it doesn't magically avoid being used. An expression protected by sizeof is accepted by both GCC and Clang.

Still, I'm wondering
1. Is the comma in a constant-expression intended to be syntactically unacceptable?

2. If acceptable, why require the parentheses? Expression could already syntactically produce constant-expression without causing other difficulties. (The comma operator has lowest grammatical precedence.) One argument for the status quo would be future language expansion.

3. Should the LHS of the comma (or more generally, any discarded-value expression) in a constant-expression context be made an unevaluated context? This would reduce SFINAE noise, as there's currently no canonical way to combine a bellwether expression with enable_if. Here are some existing alternatives:

typename std::conditional< false, decltype( expr ), return_type >::type // 1
typename std::enable_if< sizeof( expr ), return_type >::type // 2
decltype( expr, std::declval< return_type >() ) // 3
decltype(void( expr )) // 4, but no room for return_type


typename std::enable_if< ( expr, true ), return_type >::type // proposed

I think the last alternative offers more uniformity and less abuse of extraneous constructs.

Richard Smith

unread,
Sep 22, 2013, 4:05:25 AM9/22/13
to std-dis...@isocpp.org
On Sat, Sep 21, 2013 at 7:39 PM, David Krauss <pot...@gmail.com> wrote:
C++98 forbade the comma operator in a constant-expression context. C++11 makes a constant-expression a grammatical conditional-expression, which excludes the comma at the top level but allows it inside parentheses.

switch( i ) {
   
case (3,2): std::cout << "i is two\n";
}

It's fairly useless, but can be used for SFINAE where even discarded expressions are significant.

template< typename t >
auto fn( t o )
-> typename std::enable_if< ( std::declval< t >() * std::declval< t >(), true ) >::type
   
{ std::cout << o * o << '\n'; }

This use of comma should not be recommended for fully generic code: it can fail if operator, is overloaded, and the search for an overloaded operator, requires the LHS to be a complete type, which can sometimes be undesirable. Plus this doesn't work because it odr-uses std::declval<t> (also, a call to declval can't be an evaluated subexpression of a constant expression).

Note that declval or void must be used because the expression is evaluated, although nothing could potentially come of it.

template< typename t >
auto fn( t o )
-> typename std::enable_if< ( o * o, true ) >::type // Error (SFINAE?)
   
{ std::cout << o * o << '\n'; }

There doesn't seem to be implementation consensus.

This SFINAE check doesn't do what you seemed to want here; you need a pretty exotic type for this function to ever work. Something like:

struct S {}; constexpr bool operator*(S&, S&) { return true; }

Clang accepts all such examples but GCC complains that the expression is "used," even if protected by a cast to void or decltype.

Seems to be a GCC bug (EDG has a similar bug). Perhaps the ban on parameters being used in default arguments is leaking through here; I can't find any rule in the standard that would disallow an example such as the above.

SFINAE is not applied so any call to the overload set stops compilation. (Previous versions of GCC ignored the LHS of the comma under SFINAE, as well as unevaluated RHS of && and ||.)

GCC (and EDG) reject this when they see the template definition, so SFINAE is not relevant. 

David Krauss

unread,
Sep 22, 2013, 9:02:28 AM9/22/13
to std-dis...@isocpp.org
On 9/22/13 4:05 PM, Richard Smith wrote:
> This use of comma should not be recommended for fully generic code: it can
> fail if operator, is overloaded
Oh, good point. That's a corner case to be sure, but an ideal idiom
couldn't be tripped up like that.
> , and the search for an overloaded operator,
> requires the LHS to be a complete type, which can sometimes be undesirable.
> Plus this doesn't work because it odr-uses std::declval<t> (also, a call to
> declval can't be an evaluated subexpression of a constant expression).
>
> Note that declval or void must be used because the expression is evaluated,
> although nothing could potentially come of it.
Yeah. Sorry, if I'd posted my previous message sooner, you wouldn't have
had to write all that :( .
>>
>> template< typename t >
>> auto fn( t o )
>> -> typename std::enable_if< ( o * o, true ) >::type // Error (SFINAE?)
>> { std::cout << o * o << '\n'; }
>>
>> There doesn't seem to be implementation consensus.
>>
> This SFINAE check doesn't do what you seemed to want here; you need a
> pretty exotic type for this function to ever work. Something like:
>
> struct S {}; constexpr bool operator*(S&, S&) { return true; }
constexpr doesn't help because the argument to operator* is already a
parameter from fn.

I don't think it ever could work, and therefore such a template cannot
ever produce a valid instantiation, which is ill-formed with no
diagnostic required (a.k.a. undefined behavior).

Otherwise it confusingly "works half the time," i.e. when the function
isn't supposed to be selected, and on half the platforms, i.e. Clang and
others that fail to diagnose the uninstantiable template.

This story all began when someone (an occasional participant here)
suggested to me the (expr, true) "idiom," claiming it worked for him. I
tried it out in my project, noticed it "worked" in Clang but not GCC,
and shelved it without noticing its defectiveness until now, after the
first post of this thread.

Such deceptive defectiveness is probably justification enough for
outlawing it entirely. I'm now thinking there should be a rule
forbidding the comma added back to 5.19, although an overloaded comma
should be allowed.
> GCC (and EDG) reject this when they see the template definition, so
> SFINAE is not relevant.
I hadn't run enough experiments to notice that. But it's not a bug; they
actually have higher QOI in rejecting a template that could never succeed.

Gabriel Dos Reis

unread,
Sep 22, 2013, 9:11:23 AM9/22/13
to std-dis...@isocpp.org
David Krauss <pot...@gmail.com> writes:

[...]

| Such deceptive defectiveness is probably justification enough for
| outlawing it entirely. I'm now thinking there should be a rule
| forbidding the comma added back to 5.19, although an overloaded comma
| should be allowed.

I don't understand that reasoning.

-- Gaby

Richard Smith

unread,
Sep 22, 2013, 11:39:47 AM9/22/13
to std-dis...@isocpp.org


On 22 Sep 2013 06:02, "David Krauss" <pot...@gmail.com> wrote:
>
> On 9/22/13 4:05 PM, Richard Smith wrote:
>>
>> This use of comma should not be recommended for fully generic code: it can
>> fail if operator, is overloaded
>
> Oh, good point. That's a corner case to be sure, but an ideal idiom couldn't be tripped up like that.
>
>> , and the search for an overloaded operator,
>> requires the LHS to be a complete type, which can sometimes be undesirable.
>> Plus this doesn't work because it odr-uses std::declval<t> (also, a call to
>> declval can't be an evaluated subexpression of a constant expression).
>>
>> Note that declval or void must be used because the expression is evaluated,
>> although nothing could potentially come of it.
>
> Yeah. Sorry, if I'd posted my previous message sooner, you wouldn't have had to write all that :( .
>
>>>
>>> template< typename t >
>>> auto fn( t o )
>>> -> typename std::enable_if< ( o * o, true ) >::type // Error (SFINAE?)
>>>      { std::cout << o * o << '\n'; }
>>>
>>> There doesn't seem to be implementation consensus.
>>>
>> This SFINAE check doesn't do what you seemed to want here; you need a
>> pretty exotic type for this function to ever work. Something like:
>>
>> struct S {}; constexpr bool operator*(S&, S&) { return true; }
>
> constexpr doesn't help because the argument to operator* is already a parameter from fn.

That's not a problem, per the rules in [expr.const]p2. Clang accepts fn<S>…

> I don't think it ever could work, and therefore such a template cannot ever produce a valid instantiation, which is ill-formed with no diagnostic required (a.k.a. undefined behavior).
>
> Otherwise it confusingly "works half the time," i.e. when the function isn't supposed to be selected, and on half the platforms, i.e. Clang and others that fail to diagnose the uninstantiable template.
>
> This story all began when someone (an occasional participant here) suggested to me the (expr, true) "idiom," claiming it worked for him. I tried it out in my project, noticed it "worked" in Clang but not GCC, and shelved it without noticing its defectiveness until now, after the first post of this thread.
>
> Such deceptive defectiveness is probably justification enough for outlawing it entirely. I'm now thinking there should be a rule forbidding the comma added back to 5.19, although an overloaded comma should be allowed.
>
>> GCC (and EDG) reject this when they see the template definition, so SFINAE is not relevant.
>
> I hadn't run enough experiments to notice that. But it's not a bug; they actually have higher QOI in rejecting a template that could never succeed.
>
>

> --
>
> --- You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to std-discussio...@isocpp.org.
> To post to this group, send email to std-dis...@isocpp.org.
> Visit this group at http://groups.google.com/a/isocpp.org/group/std-discussion/.

David Krauss

unread,
Sep 22, 2013, 6:10:49 PM9/22/13
to std-dis...@isocpp.org
In C++03 [expr.const] disallowed the comma. C++11 has allowed it, but it
seemingly can't do anything useful unless it is overloaded.

One obvious use for the comma is enable_if< ( expr, true )>, but this
produces UB. I haven't seen any other potential use.

Since this new feature was recently added by omission and can only do
harm, it should probably be removed.

Richard Smith

unread,
Sep 22, 2013, 9:32:16 PM9/22/13
to std-dis...@isocpp.org
I disagree with your argument and your conclusion. In C++11, the comma operator is useful within constexpr functions, because we aren't allowed multiple statements:

  template<typename T>
  constexpr T my_array<T>::at(size_type n) {
    return (n < size() || throw "n too large"), (*this)[n];
  }

In C++14, it's useful in essentially all of the cases where it's useful outside of constant expressions:

  constexpr void do_stuff(int x[]) {
    for (int i = 0, j = 100; i != j; ++i, --j)
      x[i] = x[j];
  }

More philosophically, we shouldn't ban things from constant expressions simply because we're not imaginative enough to find the cases where they're genuinely useful. Constant expressions should not be a semi-random sublanguage of C++, missing random features, to the extent that we can avoid that. These days, top-level commas are prohibited mostly because constant-expressions tend to occur in contexts where a comma would mean something else.

David Krauss

unread,
Sep 22, 2013, 10:41:03 PM9/22/13
to std-dis...@isocpp.org
On 9/23/13 9:32 AM, Richard Smith wrote:
I disagree with your argument and your conclusion. In C++11, the comma
operator is useful within constexpr functions, because we aren't allowed
multiple statements:

  template<typename T>
  constexpr T my_array<T>::at(size_type n) {
    return (n < size() || throw "n too large"), (*this)[n];
  }
Great point, that's why I asked here.

But arguably, function invocation substitution doesn't produce a constant expression from that unless you parenthesize the whole expression.


In C++14, it's useful in essentially all of the cases where it's useful
outside of constant expressions:

  constexpr void do_stuff(int x[]) {
    for (int i = 0, j = 100; i != j; ++i, --j)
      x[i] = x[j];
  }
Oh wow, I didn't realize how general constant expressions have become in C++14. It's really not about syntax at all.

More philosophically, we shouldn't ban things from constant expressions
simply because we're not imaginative enough to find the cases where they're
genuinely useful. Constant expressions should not be a semi-random
sublanguage of C++, missing random features, to the extent that we can
avoid that. 
Yeah, it's the quirk of being allowed only inside parens that piqued my interest.

These days, top-level commas are prohibited mostly because
constant-expressions tend to occur in contexts where a comma would mean
something else.
Language expansion. That's the only rationale I could think of (mentioned in the second post).

There's a downside to handling expansion this way, though. If the comma is disallowed by the grammar, then disambiguation can kick in and render a declaration with syntactic constant-expression into an expression-statement. I've filed a defect report… heck you also filed a defect report (CWG 1740) about this!

It would be more responsible to explicitly reserve the comma in contexts where constant-expression is used as a grammar production. Likewise the philosophically best choice is that constant-expression and expression be grammatically the same thing.

Gabriel Dos Reis

unread,
Sep 23, 2013, 9:15:38 AM9/23/13
to std-dis...@isocpp.org
David Krauss <pot...@gmail.com> writes:

| On 9/22/13 9:11 PM, Gabriel Dos Reis wrote:
| > David Krauss <pot...@gmail.com> writes:
| >
| > [...]
| >
| > | Such deceptive defectiveness is probably justification enough for
| > | outlawing it entirely. I'm now thinking there should be a rule
| > | forbidding the comma added back to 5.19, although an overloaded comma
| > | should be allowed.
| >
| > I don't understand that reasoning.
| In C++03 [expr.const] disallowed the comma. C++11 has allowed it, but
| it seemingly can't do anything useful unless it is overloaded.

For C++11 I proposed to allow it because the restriction appeared
arbitrary and all reasons I've heard as rationale for its ban appear
very unconvincing and specious to me.

| One obvious use for the comma is enable_if< ( expr, true )>, but this
| produces UB. I haven't seen any other potential use.

I do not understand that statement about UB.

| Since this new feature was recently added by omission and can only do
| harm, it should probably be removed.

What makes you believe it was added by omission?

-- Gaby

David Krauss

unread,
Sep 24, 2013, 8:47:08 PM9/24/13
to std-dis...@isocpp.org, g...@axiomatics.org


On Monday, September 23, 2013 9:15:38 PM UTC+8, Gabriel Dos Reis wrote:
David Krauss <pot...@gmail.com> writes:

| On 9/22/13 9:11 PM, Gabriel Dos Reis wrote:
| > David Krauss <pot...@gmail.com> writes:
| >
| > [...]
| >
| > | Such deceptive defectiveness is probably justification enough for
| > | outlawing it entirely. I'm now thinking there should be a rule
| > | forbidding the comma added back to 5.19, although an overloaded comma
| > | should be allowed.
| >
| > I don't understand that reasoning.
| In C++03 [expr.const] disallowed the comma. C++11 has allowed it, but
| it seemingly can't do anything useful unless it is overloaded.

For C++11 I proposed to allow it because the restriction appeared
arbitrary and all reasons I've heard as rationale for its ban appear
very unconvincing and specious to me.

It sounds like there's already been more debate about this than I had thought.

The other sub-thread here (with Richard Smith) seemed to conclude that the status quo balances flexibility against future language expansion. My opinion is that safety for expansion only counts for syntax which is unambiguously disallowed, which the constant-expression : conditional-expression construct fails to do in cases where the parser can/must backtrack and reinterpret the constant-expression as an expression belonging to some other production rule.

But that opinion matters little, because there's technically no defect (although defects of ambiguity result and at least one DR exists), there's no feature to propose, and I'm not party to the official debate.
 
| One obvious use for the comma is enable_if< ( expr, true )>, but this
| produces UB. I haven't seen any other potential use.

I do not understand that statement about UB.

If expr evaluates an expression that cannot be used at that point, such as decltype<>() or a function parameter, then there is no possible valid specialization of the signature. Such a template is ill-formed with no diagnostic required, which is essentially a null requirement — the same as UB. The supposed idiom requires such misuse, because having more than one potential outcome (ill-formed or not) requires dependence on a template type parameter. A template non-type parameter cannot decide well-formedness in this context, aside from corner cases like divide by zero, to which the idiom doesn't apply anyway. Expressions besides template parameters need not apply because they don't decide SFINAE.

However, Richard already refuted my overall point.
 
| Since this new feature was recently added by omission and can only do
| harm, it should probably be removed.

What makes you believe it was added by omission?

I simply mean that comma was forbidden in C++98, both by the grammar and by an additional [expr.const] requirement. It was added for C++11 by the removal (omission) of the [expr.const] requirement yet the grammar wasn't adjusted likewise.

As I mentioned in the other subthread, it would be "cleaner" to produce constant-expression : expression and explicitly reserve the comma in productions forming e.g. ( constant-expression ).
Reply all
Reply to author
Forward
0 new messages