What Is Decltype with Two Arguments

What is decltype with two arguments?

It's an comma-separated list of expressions, the type is identical to the type of the last expression in the list. It's usually used to verify that the first expression is valid (compilable, think SFINAE), the second is used to specify that decltype should return in case the first expression is valid.

Decltype with two arguments modifies type

You are looking at , which is an operator in C++ (comma operator). See row 16

Its declaration may looks like this:
T2& operator,(const T& a, T2& b);

So a, b, c is evaluate as ((a,b),c), (b,c), (c), which returns a reference to the return type of c (a, b, c can be expressions)

In your case 1, i returns a reference to the last term, i. Hence the type is int&, so decltype(1, i) var = i; becomes int& var = i;

(i) is an expression, and it has return value which is a reference to i (int&)

decltype with two parameters, = decltype(a,b), for function return type

In the expression (void)(c.*f)(), void():

  • (void)(c.*f)() serves to check that f is a member function in c that can be called with no arguments; it doesn't matter what the member-function return type is anyway, but it's nominally cast to void

  • if the above is valid, the comma operator discards it and considers the second part, such that the overall effect is per decltype(void()), which yields a void type

Praetorian comments below that the trailing , void() is redundant, as the leading part is cast to void (the C-style cast (void)) anyway... I suspect the , void() is intended as documentation, highlighting the enable_if-like conditional selection of a return type, it's a style choice whether to shorten that further to decltype((c.*f)(), void()).

Further details / example

This can be used for SFINAE, though enable_if is more self-documenting. Consider this code, and the comments in main() (CT stands for Compile Time):

#include <iostream>

template<class C, class F>
auto test(C c, F f) -> decltype((void)(c.*f)(), void())
{ std::cout << "member function\n"; }

template<class C>
void test(C c, int)
{ std::cout << "int\n"; }

struct X {
int f() { return 42; }
double g(int) { return 3.14; }
};

int main()
{
X x;
test(x, &X::f); // ok - outputs "member function\n"
// test(x, &X::g); // CT error - g needs an argument
test(x, 99); // ok - outputs "int\n"
}

Output:

member function
int

You can see and run the code here.

Meaning of `decltype` with more than one argument

Comma here is the operator, not argument separator

decltype with function template which has default argument make the confused result(a funny problem or gcc's bug)

Let us consider just the "update" section. You are relying on a very dangerous property - a state of the type system computation. Namely, create(state<2>) remains undefined, until a seemingly unrelated struct generate_state<2> is instantiated.

Any sane type system in any respectable language is (or should be) stateless. A given type expression is a constant throughout the whole compilation process. With it, the compiler can employ complex reasoning algorithms to match types and check the correctness of the program.

The mechanism you use defy this. Such approach can lead to really weird results. A perfect question: Is stateful metaprogramming ill-formed (yet)? shows what it can lead to:

static_assert(!std::is_same_v<S<>, S<>>, "This is ridiculous");

to be actually accepted by the compiler! (follow the link above to see full example, I don't want to copy-paste it here).

So in short: don't use it! If you want to be able to switch between different implementations using type system, use stateless aproach, as shown in the other answer of mine (which I leave for reference).

Different compilers seem to work in different ways when stateful type computation is encountered. You are at the mercy of their internals. The decltype scenario of yours show weird behavior of the g++ implementation. It seems that within the context of decltype it is actually able to instantiate auto create(state<N>) as if it was a stand-alone template.

This compiles with g++ 9.2:

int main() {
using t = decltype(getvalue(0, state<2>{}));
std::cout << typeid(t).name() << std::endl;
auto result = getvalue(0, state<2>{});
std::cout << typeid(decltype(result)).name() << std::endl;
}

https://godbolt.org/z/HdtKFd

The decltype(getvalue(0, state<2>{})) manages to instantiate create<2> and then the auto result = getvalue(0, state<2>{}) successfully compiles, using #22. However, if you comment out the first 2 lines, the 3-rd line suddenly switches to #11 and fails.

So, what the standard says about it? Not much. Probably because it is hard to specify precisely what should be considered ill-formed. Check out this answer for a bit more elaborate answer: https://stackoverflow.com/a/44268181/635654

Dependent type or argument in decltype in function definition fails to compile when declared without decltype

Because when there is a template parameter involved, decltype returns an unqiue dependent type according to the standard, see below. If there is no template parameter then it resolves to an obvious size_t. So in this case you have to choose either both declaration and definition have an independent expression (e.g. size_t/decltype(sizeof(int))), as a return type, or both have dependent expression (e.g. decltype(sizeof(T))), which resolved to an unique dependent type and considered to be equivalent, if their expressions are equivalent (see below).

In this post I am using the C++ standard draft N3337.

§ 7.1.6.2 [dcl.type.simpl]

¶ 4

The type denoted by decltype(e) is defined as follows:
— if e is an unparenthesized id-expression or an unparenthesized class member access (5.2.5), 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 func-
tions, 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.

This explains what is decltype(sizeof(int)). But for decltype(sizeof(T)) there is another section explaining what it is.

§ 14.4 [temp.type]

¶ 2

If an expression e involves a template parameter, decltype(e) denotes a unique dependent type. Two such
decltype-specifiers refer to the same type only if their expressions are equivalent (14.5.6.1). [ Note: however,
it may be aliased, e.g., by a typedef-name. — end note ]

In Clang LLVM sources version 3.9 in file lib/AST/Type.cpp

DecltypeType::DecltypeType(Expr *E, QualType underlyingType, QualType can)
// C++11 [temp.type]p2: "If an expression e involves a template parameter,
// decltype(e) denotes a unique dependent type." Hence a decltype type is
// type-dependent even if its expression is only instantiation-dependent.
: Type(Decltype, can, E->isInstantiationDependent(),
E->isInstantiationDependent(),
E->getType()->isVariablyModifiedType(),
E->containsUnexpandedParameterPack()),

The important phrase starts as "Hence a decltype...". It again clarifies the situation.

Again in Clang sources version 3.9 in file lib/AST/ASTContext.cpp

QualType ASTContext::getDecltypeType(Expr *e, QualType UnderlyingType) const {
DecltypeType *dt;

// C++11 [temp.type]p2:
// If an expression e involves a template parameter, decltype(e) denotes a
// unique dependent type. Two such decltype-specifiers refer to the same
// type only if their expressions are equivalent (14.5.6.1).
if (e->isInstantiationDependent()) {
llvm::FoldingSetNodeID ID;
DependentDecltypeType::Profile(ID, *this, e);

void *InsertPos = nullptr;
DependentDecltypeType *Canon
= DependentDecltypeTypes.FindNodeOrInsertPos(ID, InsertPos);
if (!Canon) {
// Build a new, canonical typeof(expr) type.
Canon = new (*this, TypeAlignment) DependentDecltypeType(*this, e);
DependentDecltypeTypes.InsertNode(Canon, InsertPos);
}
dt = new (*this, TypeAlignment)
DecltypeType(e, UnderlyingType, QualType((DecltypeType *)Canon, 0));
} else {
dt = new (*this, TypeAlignment)
DecltypeType(e, UnderlyingType, getCanonicalType(UnderlyingType));
}
Types.push_back(dt);
return QualType(dt, 0);
}

So you see Clang gathers and picks those unique dependent types of decltype in/from a special set.

Why compiler is so stupid that it does not see that the expression of decltype is sizeof(T) that is always size_t? Yes, this is obvious to a human reader. But when you design and implement a formal grammar and semantic rules, especially for such complicated languages as C++, you have to group problems up and define the rules for them, rather than just come up with a rule for each particular problem, in the latter way you just wont be able to move with your language/compiler design. The same here there is no just rule: if decltype has a function call expression that does not need any template parameters resolution - resolve decltype to the return type of the function. There is more than that, there are so many cases you need to cover, that you come up with a more generic rule, like the quoted above from the standard (14.4[2]).

In addition, a similar non-obvious case with auto, decltype(auto) found by AndyG in C++-14 (N4296, § 7.1.6.4 [dcl.spec.auto] 12/13):

§ 7.1.6.4 [dcl.spec.auto]

¶ 13

Redeclarations or specializations of a function or function template with a declared return type that uses a
placeholder type shall also use that placeholder, not a deduced type. [ Example:

auto f();
auto f() { return 42; } // return type is int
auto f(); // OK
int f(); // error, cannot be overloaded with auto f()
decltype(auto) f(); // error, auto and decltype(auto) don’t match

Changes in C++17, Document Number >= N4582

Change in the standard draft N4582 from March 2016 (thanks to bogdan) generalizes the statement:

§ 17.4 (old § 14.4) [temp.type]

¶ 2

If an expression e is type-dependent (17.6.2.2), decltype(e) denotes a unique dependent type. Two such
decltype-specifiers refer to the same type only if their expressions are equivalent (17.5.6.1). [ Note: however,
such a type may be aliased, e.g., by a typedef-name. — end note ]

This change leads to another section describing the type dependent expression that looks quite strange to our particular case.

§ 17.6.2.2 [temp.dep.expr] (old § 14.6.2.2)

¶ 4

Expressions of the following forms are never type-dependent (because the type of the expression cannot be
dependent):

...
sizeof ( type-id )
...

There are further sections on value-dependent expressions where sizeof can be value-dependent if the type-id dependent. There is no relation between value-dependent expression and decltype. After some thinking, I did not find any reason why decltype(sizeof(T)) must not or cannot resolve into size_t. And I would assume that was quite sneaky change ("involves a template parameter" to "type-dependent") in the standard that compiler developers did not pay much attention to (maybe overwhelmed by many other changes, maybe did not think that it might actually change something, just a simple formulation improvement). The change does make sense, because sizeof(T) is not type-dependent, it is value-dependent. decltype(e)'s operand is a unevaluated operand, i.e. does not care about value, only about type. That is why decltype returns a unique type only when e is type-dependent. sizeof(e) might be only value-dependent.

I tried the code with clang 5, gcc 8 -std=c++1z - the same result: error. I went further and tried this code:

template <typename>
struct Cls {
static std::size_t f();
};

template <typename T>
decltype(sizeof(sizeof(T))) Cls<T>::f() {
return 0;
}

The same error was given, even that sizeof(sizeof(T)) is neither type- or value-dependent (see this post). This gives me a reason to assume that the compilers are working in an old way of C++-11/14 standard (i.e. "involves a template parameter") like in the source snippet above from clang 3.9 source (I can verify that the latest developing clang 5.0 has the same lines, have not found anything related to this new change in the standard), but not type-dependent.

How to enable auto nontype template parameter pack for same type of parameters

You can make use of the decltype construct as shown below:

template<auto T1, decltype(T1)... Args> struct Custom
{
};

Working Demo



Related Topics



Leave a reply



Submit