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.

Leave a Comment