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
auto is a keyword in C++11 and later that is used for automatic type deduction. The decltype type specifier yields the type of a specified expression. Unlike auto that deduces types based on values being assigned to the variable, decltype deduces the type from an expression passed to it.
In the C++ programming language, decltype is a keyword used to query the type of an expression. Introduced in C++11, its primary intended use is in generic programming, where it is often difficult, or even impossible, to express types that depend on template parameters.
decltype returnsIf what we pass to decltype is the name of a variable (e.g. decltype(x) above) or function or denotes a member of an object ( decltype x.i ), then the result is the type of whatever this refers to. As the example of decltype(y) above shows, this includes reference, const and volatile specifiers.
Decltype keyword in C++ Decltype stands for declared type of an entity or the type of an expression. It lets you extract the type from the variable so decltype is sort of an operator that evaluates the type of passed expression. SYNTAX : decltype( expression )
Prior to C++17, when writing a template non-type parameter, you had to specify its type first. So a common pattern became writing something like: template <class T, T N> struct integral_constant { using type = T; static constexpr T value = N; }; using five = integral_constant<int, 5>;
In C++14, you can use decltype(auto) with no trailing return type to declare a template function whose return type depends on the types of its template arguments.
decltype (C++) 1 Return Value. The type of the expression parameter. 2 Remarks. The decltype type specifier is supported in Visual Studio 2010 or later versions, and can be used with native or managed code. ... 3 Decltype and Auto. ... 4 Examples. ... 5 Requirements. ...
For example, consider the following code example in which the return type of the template function depends on the types of the template arguments. In the code example, the UNKNOWN placeholder indicates that the return type cannot be specified.
It's all in the rules describing how decltype
works.
[dcl.type.simple]
4 For an expression
e
, the type denoted bydecltype(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 bye
. 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)
isT&&
, whereT
is the type ofe
;otherwise, if e is an lvalue,
decltype(e)
isT&
, whereT
is the type ofe
;otherwise,
decltype(e)
is the type ofe
.
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 asconst
. Such an object shall have literal type and shall be initialized. In anyconstexpr
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With