32

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?

Ryan Haining
  • 35,360
  • 15
  • 114
  • 174
  • Because having a dependent type anywhere in a type expression makes the whole type dependent, despite it being in fact a constant. – n. m. could be an AI May 31 '17 at 22:06
  • @n.m. That's not true. – T.C. May 31 '17 at 22:07
  • 6
    One version is ill-formed if `T` is incomplete. The other isn't. – T.C. May 31 '17 at 22:08
  • Fails in gcc as well "error: prototype for ‘decltype (sizeof (T)) Cls< >::f()’ does not match any in class ‘Cls< >’" – alfC May 31 '17 at 22:14
  • 2
    Vaguely related: g++ thinks `sizeof` a non-type template parameter is dependent https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80368 . I imagine something similar may be going on here. – aschepler May 31 '17 at 22:20
  • @T.C. indeed the standard says sizeof() is never type-dependent. – n. m. could be an AI May 31 '17 at 22:22
  • I tend to think that all of these are compiler bugs. Both compilers, g++ and clang++, fail to correctly decide type equality for these pairs of type expressions. – nickie Jun 05 '17 at 20:14
  • @aschepler there are two sorts of dependent, *type-dependent* and *value-dependent*. If `T` is type-dependent then `sizeof(T)` is value-dependent but not type-dependent. – M.M Jun 06 '17 at 01:43
  • This also gives the error in g++, without sizeof: `struct S{ int f(); }; template decltype(N) S::f() { }` – M.M Jun 06 '17 at 01:45
  • It's not clear to me which part of the standard covers matching of the function definition to prototype, for the case of member functions of class templates that have not yet been instantiated – M.M Jun 06 '17 at 01:59
  • I want to make sure i am reading this right... So you initiate a template with the proper class but don't tell anything about what it is... You proceed to make a struct that has function which i assume will return the byte size of an object. You then proceed to declare T to be of a typename, but then you try to pass it to the sizeOf command which doesn't return any of the declare types accepted values because it doesn't know what T is to begin with. My interpretation is that you try to pass the type to be unknown to the function yet you ambiguously declare it to have an integer type with f()! – Jouster500 Jun 06 '17 at 14:45
  • @Jouster500 `decltype(sizeof(T))` would be `size_t` for anything. – Ryan Haining Jun 06 '17 at 19:12
  • @M.M that example failing makes sense though since `f` isn't declared as a template, but I still see it with `struct S { template int f(); };` – Ryan Haining Jun 06 '17 at 19:12
  • @RyanHaining maybe language-lawyer tag would be a good idea – M.M Jun 06 '17 at 22:09
  • @M.M added, I wasn't initially sure if I was missing something more obvious – Ryan Haining Jun 06 '17 at 22:40

1 Answers1

11

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.

Yuki
  • 3,857
  • 5
  • 25
  • 43
  • 1
    Huh. I'm surprised it's "involves a template parameter" rather than "is type-dependent", but there you have it. – aschepler Jun 07 '17 at 13:26
  • I don't totally follow, so perhaps you can clarify for me how this answers the question. Since `sizeof` is never a type-dependent expression, I thought it should always be returning a `std::size_t`, and the fact that a `decltype` is used on the outer expression shouldn't matter? – AndyG Jun 07 '17 at 14:27
  • 2
    @AndyG Does `sizeof(T)` involves a template parameter? Yes. The quote from the standard presented in the answer: "If an expression e involves a template parameter, decltype(e) denotes a unique dependent type." It is for you as a human who know C++ obvious that `sizeof` always returns `size_t` and the problem solved. But when you are writing a formal grammar and semantic rules there is more than this kind of simple rules. – Yuki Jun 07 '17 at 14:33
  • 3
    @Yuki: Thanks, that clears it up. I think more intuition can be found in the C++14 standard N4296 [dcl.spec.auto]/13 (and partly 12) which states "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." The intuition is that `decltype(sizeof(T))` is a placeholder type until the template is instantiated ([dcl.spec.auto]/12), and the compiler can already see that you've mixed an explicit type and a placeholder. Thinking of it that way helps, although it's a little imprecise – AndyG Jun 07 '17 at 15:04
  • 3
    @Yuki *"It is for you as a human who know C++ obvious that sizeof always returns size_t and the problem solved."* I'm not so sure about this statement - `decltype(sizeof(T))` for `T=void` should silencely force sfinea'ing out... [example](https://wandbox.org/permlink/WtuMBtWCckhn32ux) – W.F. Jun 07 '17 at 16:14
  • 2
    Note that the paragraph you quoted from [temp.type] was changed by the resolution of [DR 2064](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#2064), which was adopted into the working draft in March 2016 (N4582). The expression now has to be type-dependent. – bogdan Jun 07 '17 at 16:48
  • @W.F. You are correct if you are you using it in some SFINAE constructs, but I think this in not the case for the current case. Thank you anyway for mentioning this. – Yuki Jun 07 '17 at 16:54
  • hmm @n.m's comment above reads "*the standard says sizeof() is never type-dependent.*" so should this all be legal in c++17 if that's true? – Ryan Haining Jun 07 '17 at 17:09
  • The standard (C++-17 is still developing) is growing, getting more complicated and sometimes confusing. I would not expect the behavior to change and the latest clang verifies my guess. For the current standard 14 as well as for the previous 11 it is settled. – Yuki Jun 07 '17 at 18:35
  • 1
    Since the explanation given in the answer is no longer supported by the current form of that paragraph (and compilers apply DRs to previous standard versions as well), I think a better explanation for why this won't work is in [\[temp.over.link\]](http://eel.is/c++draft/temp.over.link), from §4 onwards. The wording could be improved, but I think these are the rules that compilers use to match those declarations with one another. Note that the ODR requirements are pretty strict - *the same sequence of tokens*. cc @RyanHaining – bogdan Jun 09 '17 at 11:21
  • @bogdan Explanation in the answer is totally based on the current official version of the standard, and it considers the currently developing (not-official) version of the standard. The section you linked talks about template function overloading and does not give direct answer to the problem, however it helps to understand the idea why it might not work and how the standard (and compilers eventually) handles template. – Yuki Jun 09 '17 at 11:50
  • @bogdan is a member function of a class template bound to the same rules as a free function template? – Ryan Haining Jun 09 '17 at 14:24
  • @RyanHaining Yes, they do, there is no distinction between member function template overloading and free function template overloading, it is just function template overloading. This section is about template parameters and what is considered to equivalent and what is not. – Yuki Jun 09 '17 at 17:14
  • @Yuki I'm not asking about member function template vs free function template, I'm asking about member function *of a class template* – Ryan Haining Jun 09 '17 at 17:33
  • 1
    @RyanHaining Not in general, of course, but for the purpose of linking different declarations together, given [\[temp.class\]/3](http://eel.is/c++draft/temp.class#3) (*the member definition is defined as a template definition in which the template-parameters are those of the class template*) and [temp.mem.func] below, I'd say it's pretty close. As I said, I think the wording could be improved there; nothing unusual for the [temp] clause, really :-). – bogdan Jun 09 '17 at 18:05
  • @RyanHaining Function template overloading cannot exist on functions that do not have template parameters (i.e. that are not function templates, but just functions). The class has template parameters, but not the functions. The section [temp.over.link] is relevant to the original topic only when there is a question of two `expression`s in two `decltype`s, and it answers if those `expression`s are equivalent. Otherwise, I think, the answer for the original problem is in [temp.type](http://eel.is/c++draft/temp.type)[2]: `decltype()` is a unique type. – Yuki Jun 09 '17 at 18:28
  • I did a little bit more thinking, see the latest answer. – Yuki Jun 09 '17 at 21:47