Using decltype(auto) in C++ non-type template parameter

2.3k Views Asked by At

I am learning C++17 new feature decltype(auto) for non-type template parameter. I wrote a simple code snippet as following:

#include <type_traits>

template<decltype(auto) arg>
struct Foo {};

int
main()
{
    constexpr int x = 42;
    static_assert(std::is_same_v<Foo<42>, Foo<x>>);
}

As I understand, Foo<42> should be the same type as Foo<x>.

However, The static_assert statement compiles with clang++, MSVC 19.27 but fails with GCC 10.2, MSVC 19.25.

My question is: Why do compilers behave differently? What does the Standard say about this?

Link to Compiler Explorer:

clang++ https://godbolt.org/z/66M695

gcc https://godbolt.org/z/3v5Mhd

MSVC 19.25 https://godbolt.org/z/qP6v89

MSVC 19.27 https://godbolt.org/z/14aK5Y

3

There are 3 best solutions below

17
On

It's all in the rules describing how decltype works.

[dcl.type.simple]

4 For an expression e, the type denoted by decltype(e) is defined as follows:

  • if e is an unparenthesized id-expression naming a structured binding ([dcl.struct.bind]), decltype(e) is the referenced type as given in the specification of the structured binding declaration;

  • otherwise, if e is an unparenthesized id-expression or an unparenthesized class member access, decltype(e) is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed;

  • otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e;

  • otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;

  • otherwise, decltype(e) is the type of e.

When using decltype(auto), e is the expression that is used as an initializer for our object (arg). In the OP, this expression is x. It's an unparenthesized id-expression, so decltype(x) would be the type of the entity named by x. That type is int const, because a constexpr specifier implies const.

[dcl.constexpr]

9 A constexpr specifier used in an object declaration declares the object as const. Such an object shall have literal type and shall be initialized. In any constexpr variable declaration, the full-expression of the initialization shall be a constant expression.

So here's a cute modification to your code sample that makes GCC accept it.

static_assert(std::is_same_v<Foo<42>, Foo<+x>>);

Why is that? It's because +x is no longer an id-expression. It's a plain old prvalue expression of type int, having the same value as x. So that's what decltype(auto) deduces, an int.

All in all, the compilers that rejected your code are acting correctly. And I guess it goes to show you that using decltype(auto) for a non-type template parameter should come with a short disclaimer.

16
On

I think this is a gcc bug, and the static_assert should pass.

According to this:

If the type of a template-parameter contains a placeholder type, the deduced parameter type is determined from the type of the template-argument by placeholder type deduction. ...

Placeholder type deduction in this context mean the type of the parameter is deduced as if deduced by these invented declarations:

decltype(auto) t = 42; // for Foo<42> : int
decltype(auto) t = x;  // for Foo<x> : int const

And then, according to this:

A non-type template-parameter shall have one of the following (optionally cv-qualified) types:

...

(4.6) a type that contains a placeholder type.

The top-level cv-qualifiers on the template-parameter are ignored when determining its type.

Since the top-level qualifiers are ignored when determining the type according to the invented declarations, both Foo<42> and Foo<x> should have the same type, and the static_assert should pass.

6
On

In C++17, I think GCC is wrong due to the following rules:
temp.type#1

Two template-ids refer to the same class, function, or variable if

1.1 their template-names, operator-function-ids, or literal-operator-ids refer to the same template and
[...]
1.3 their corresponding non-type template arguments of integral or enumeration type have identical values

Formally, a name is used to refer to the entity
basic.concept#5

Every name that denotes an entity is introduced by a declaration.

So, whether Foo<42> or Foo<x>, their template-names all refer to the entity template<decltype(auto) arg> struct Foo {}; we declared. So the bullet 1.1 is first satisfied. Obviously, in this example, the corresponding template-arguments have the identical values, namely 42. At least, according to what the c++17 standard says, they are the equivalence type. Hence GCC is wrong. In addition, GCC 7.5 agrees these types are equivalence.


However, something is changed in the latest draft. It introduces a new wording "template-argument-equivalent".
temp.type#1

Two template-ids are the same if

1.1 their template-names, operator-function-ids, or literal-operator-ids refer to the same template, and
1.2 ...
1.3 their corresponding non-type template-arguments are template-argument-equivalent (see below) after conversion to the type of the template-parameter

And
template-argument-equivalent

Two values are template-argument-equivalent if they are of the same type and

they are of integral type and their values are the same

As said in other answers, the deduced type for Foo<42> is int. Instead, the deduced type for Foo<x> is int const. Although their deduced types are different, However, such a rule should be obeyed:

The top-level cv-qualifiers on the template-parameter are ignored when determining its type.

Hence after conversion to the type of the template-parameter, these two values are of the same type, so they are template-argument-equivalent. So, talking this example under the c++20 standard, GCC is still wrong.