关于c ++:函数定义中的decltype中的依赖类型或参数在没有decltype的情况下声明时无法编译

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

我一直在尝试在解析为与声明相同类型的定义中使用推断的返回类型。这工作:

1
2
3
4
5
6
7
template <typename>
struct Cls {
  static std::size_t f();
};

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

但是如果我把定义改为与sizeof(T)相同的定义,用sizeof(T)替换sizeof(int),它就失败了。

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

GCC的错误(clang几乎相同):

1
2
3
4
5
6
error: prototype fordecltype (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();
                        ^

同样的问题也出现在函数参数类型上:

1
2
3
4
5
6
7
template <typename>
struct Cls {
  static void f(std::size_t);
};

template <typename T>
void Cls<T>::f(decltype(sizeof(T))) { } // sizeof(int) works instead

更奇怪的是,如果声明和定义匹配,并且都使用decltype(sizeof(T))编译成功,我可以static_assert返回类型是size_t。以下编译成功:

1
2
3
4
5
6
7
8
9
10
11
#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())>{},"");

用另一个例子更新。这不是依赖类型,但仍然失败。

1
2
3
4
5
6
7
template <int I>
struct Cls {
  static int f();
};

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

如果我在定义和声明中同时使用decltype(I),如果我在定义和声明中同时使用int,它是有效的,但是两种不同的方法都失败了。

更新2:类似的例子。如果Cls改为非类模板,则编译成功。

1
2
3
4
5
6
7
8
template <typename>
struct Cls {
  static int f();
  using Integer = decltype(Cls::f());
};

template <typename T>
typename Cls<T>::Integer Cls<T>::f() { return I; }

更新3:M.M.中的另一个失败示例是非模板类的模板化成员函数。

1
2
3
4
5
6
7
struct S {
  template <int N>
  int f();
};

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

为什么声明和定义只与依赖类型不一致是非法的?即使类型本身与上面的template 不一样,为什么它也会受到影响?


因为当涉及模板参数时,decltype会根据标准返回一个非qiue依赖的类型,见下文。如果没有模板参数,则解析为一个明显的size_t。因此,在这种情况下,您必须选择声明和定义都有一个独立的表达式(如size_t/decltype(sizeof(int)))作为返回类型,或者两者都有依赖表达式(如decltype(sizeof(T))),如果它们的表达式是等效的(见下文),则它们解析为唯一的依赖类型并被视为等效的。

在这篇文章中,我使用C++标准草案N33 37。

§ 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.

这就解释了什么是decltype(sizeof(int))。但对于decltype(sizeof(T))来说,还有一部分解释它是什么。

§ 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 ]

在clang llvm源中,文件lib/AST/Type.cpp中的版本3.9

1
2
3
4
5
6
7
8
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()),

重要的短语开头是"因此是decltype…"。它再次澄清了形势。

在clang sources 3.9版文件lib/AST/ASTContext.cpp中再次出现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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);
}

所以你可以看到clang收集并从一个特殊的集合中选择那些独特的依赖类型decltype

为什么编译器如此愚蠢以至于它看不到decltype的表达式是sizeof(T),它总是size_t?是的,这对人类读者来说是显而易见的。但是当你设计并实现一个正式的语法和语义规则时,尤其是对于C++这样复杂的语言,你必须将问题分组起来,并为它们定义规则,而不是仅仅为每个特定的问题提出一个规则,在后一种方式中,你就不能用语言/编译器设计来移动。这里也有同样的规则:如果decltype有一个不需要任何模板参数解析的函数调用表达式-将decltype解析为函数的返回类型。除此之外,还有许多情况需要您进行讨论,因此您可以提出一个更通用的规则,如上面从标准(14.4[2]中引用的规则)。

此外,在AndyG+14(N496,〈7.1.64.[DCL,SPEC.AUTO)12/13)中发现了一个与EDCOX1〔15〕、EDCOX1〔16〕相似的不明显的情况。

§ 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:

1
2
3
4
5
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

号C++ 17、文档号> N45的变化

自2016年3月起,N4582标准草案的变更(得益于Bogdan)概括了以下声明:

§ 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 ]

这一变化导致了另一个描述类型相关表达式的部分,这种表达式在我们的特定情况下看起来很奇怪。

§ 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):

1
2
3
...
sizeof ( type-id )
...

还有一些关于值相关表达式的章节,其中,如果type-id相关,sizeof可以是值相关表达式。依赖值表达与decltype之间没有关系。经过一番思考,我没有找到任何理由解释为什么decltype(sizeof(T))不能或不能解决成size_t。在编译器开发人员不太关注的标准中,我认为这是一个非常微妙的变化("涉及到模板参数"到"依赖类型")(可能被许多其他的变化所压倒,可能认为它实际上并没有改变什么,只是一个简单的公式改进)。这种变化确实有意义,因为sizeof(T)不依赖于类型,而是依赖于值。decltype(e)的操作数是未计算的操作数,即不关心值,只关心类型。这就是为什么只有当e依赖于类型时,decltype才返回唯一的类型。sizeof(e)可能只依赖于值。

我用clang 5,gcc 8 -std=c++1z尝试了代码——同样的结果:错误。我进一步尝试了以下代码:

1
2
3
4
5
6
7
8
9
template <typename>
struct Cls {
  static std::size_t f();
};

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

给出了同样的错误,即使sizeof(sizeof(T))既不依赖于类型也不依赖于值(见本帖)。这给了我一个理由,假设编译器以C++ 11/14标准(即"涉及模板参数")的旧方式工作,如从CLAN 3.9源代码中的源代码片段(我可以验证最新开发的CLAN 5具有相同的行,没有发现任何与此标准中的新变化相关的),但不是DE类型。悬而未决。