Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I've been playing with deduced return types in definitions that resolve to the same type as the declaration. This works:

template <typename> struct Cls {   static std::size_t f(); };  template <typename T> decltype(sizeof(int)) Cls<T>::f()  { return 0; } 

But if I change the definition to something that should be equivalent by replacing sizeof(int) with sizeof(T) it fails

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

gcc's error (clang is almost identical):

error: prototype for ‘decltype (sizeof (T)) Cls<T>::f()’ does not match any in class ‘Cls<T>’  decltype(sizeof(T)) Cls<T>::f() { return 0; }                      ^~~~~~ so.cpp:4:24: error: candidate is: static std::size_t Cls<T>::f()      static std::size_t f();                         ^ 

The same problem arises with function parameter types:

template <typename> struct Cls {   static void f(std::size_t); };  template <typename T> void Cls<T>::f(decltype(sizeof(T))) { } // sizeof(int) works instead 

Stranger yet, if the declaration and definition match and both use decltype(sizeof(T)) it compiles successfully, and I can static_assert that the return type is size_t. The following compiles successfully:

#include <type_traits>  template <typename T> struct Cls {   static decltype(sizeof(T)) f(); };  template <typename T> decltype(sizeof(T)) Cls<T>::f() { return 0; }  static_assert(std::is_same<std::size_t, decltype(Cls<int>::f())>{}, ""); 

Update with another example. This isn't a dependent type but still fails.

template <int I> struct Cls {   static int f(); };  template <int I> decltype(I) Cls<I>::f() { return I; } 

If I use decltype(I) in both the definition and declaration it works, if I use int in both the definition and declaration it works, but having the two differ fails.


Update 2: A similar example. If Cls is changed to not be a class template, it compiles successfully.

template <typename> struct Cls {   static int f();   using Integer = decltype(Cls::f()); };  template <typename T> typename Cls<T>::Integer Cls<T>::f() { return I; } 

Update 3: Another failing example from M.M. A templated member function of a non-templated class.

struct S {   template <int N>   int f(); };  template <int N> decltype(N) S::f() {} 

Why is it illegal for the declaration and definition to disagree with a dependent type only? Why is it affected even when the type itself isn't dependent as with the template <int I> above?

like image 564
Ryan Haining Avatar asked May 31 '17 21:05

Ryan Haining


People also ask

What does decltype function do in C++?

The decltype type specifier yields the type of a specified expression. The decltype type specifier, together with the auto keyword, is useful primarily to developers who write template libraries. Use auto and decltype to declare a template function whose return type depends on the types of its template arguments.

What does decltype stand for?

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 )

What does decltype auto do?

decltype(auto) is primarily useful for deducing the return type of forwarding functions and similar wrappers, where you want the type to exactly “track” some expression you're invoking.

What does decltype return?

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.


1 Answers

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.

like image 91
Yuki Avatar answered Oct 19 '22 14:10

Yuki