首页 文章

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

提问于
浏览
28

我一直在玩定义中的推断返回类型,它们解析为与声明相同的类型 . 这有效:

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

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

但是,如果我将定义更改为应该等效的东西,将 sizeof(int) 替换为 sizeof(T) it fails

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

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

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();
                        ^

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

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 . 以下编译成功:

#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())>{}, "");

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

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

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

如果我在定义和声明中使用 decltype(I) 它是有效的,如果我在定义和声明中使用 int 它可以工作,但是两者不同就失败了 .


更新2:一个类似的例子 . 如果 Cls 更改为不是类模板,则会成功编译 .

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.的另一个失败的例子 . 非模板类的模板化成员函数 .

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

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

为什么声明和定义只是不同意依赖类型是非法的?即使类型本身不依赖于 template <int I> ,它为什么会受到影响?

1 回答

  • 9

    因为当涉及模板参数时, decltype 根据标准返回unqiue依赖类型,见下文 . 如果没有模板参数,那么它会解析为明显的 size_t . 所以在这种情况下你必须选择声明和定义都有一个独立的表达式(例如 size_t/decltype(sizeof(int)) ),作为返回类型,或者两者都有依赖表达式(例如 decltype(sizeof(T)) ),它们解析为唯一的依赖类型并被认为是等价的,如果他们的表达是相同的(见下文) .

    在这篇文章中,我使用的是C标准草案N3337 .

    §7.1.6.2[dcl.type.simpl]¶4decltype(e)表示的类型定义如下: - 如果e是未加密的id-expression或未加密的类成员访问(5.2.5),则decltype( e)是由e命名的实体的类型 . 如果没有这样的实体,或者e命名了一组过载的功能,那么该程序就会形成错误; - 否则,如果e是x值,则decltype(e)是T &&,其中T是e的类型; - 否则,如果e是左值,则decltype(e)是T&,其中T是e的类型; - 否则,decltype(e)是e的类型 .

    这解释了什么是 decltype(sizeof(int)) . 但是对于 decltype(sizeof(T)) ,还有另一部分解释它是什么 .

    §14.4[temp.type]¶2如果表达式e涉及模板参数,则decltype(e)表示唯一的依赖类型 . 两个这样的decltype-specifiers只有在它们的表达式是等价的时才引用相同的类型(14.5.6.1) . [注意:但是,它可能是别名,例如,通过typedef-name . - 结束说明]

    在Clang LLVM源文件中的3.9版 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()),
    

    这个重要的短语开头是“因此是一个类型......” . 它再次澄清了这种情况 .

    再次在Clang源文件中的3.9版 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);
    }
    

    所以你看到Clang收集并从特殊集中选择 decltype 中那些独特的依赖类型 .

    为什么编译器是如此愚蠢以至于它没有看到 decltype 的表达式 sizeof(T) 始终是 size_t ?是的,这对于人类读者来说是显而易见的 . 但是当你设计和实现一个正式的语法和语义规则时,特别是对于像C这样的复杂语言,你必须将问题分组并为它们定义规则,而不是仅针对每个特定问题提出规则,后者你不能用你的语言/编译器设计移动的方式 . 这里没有相同的规则:如果 decltype 有一个函数调用表达式,不需要任何模板参数解析 - 将 decltype 解析为函数的返回类型 . 除此之外,还有很多情况需要涵盖,你提出了一个更通用的规则,就像标准中引用的那样( 14.4[2] ) .

    此外,AndyG在C-14中发现了一个类似的非显而易见的案例 autodecltype(auto) (N4296,§7.1.6.4[dcl.spec.auto] 12/13):

    §7.1.6.4[dcl.spec.auto]¶13具有使用占位符类型的声明返回类型的函数或函数模板的重新声明或特化也应使用该占位符,而不是推导出的类型 . [例子:auto f();
    auto f(){return 42; } //返回类型是int
    auto f(); // 好
    int f(); //错误,不能用auto f()重载
    decltype(auto)f(); //错误,auto和decltype(auto)不匹配

    C 17中的变更,凭证编号> = N4582

    自2016年3月起更改标准草案N4582(感谢bogdan)概括了以下声明:

    §17.4(旧§14.4)[temp.type]¶2如果表达式e是类型相关的(17.6.2.2),则decltype(e)表示唯一的依赖类型 . 两个这样的decltype-specifiers只有在它们的表达式是等价的时才引用相同的类型(17.5.6.1) . [注意:但是,这种类型可能是别名,例如,通过typedef-name . - 结束说明]

    此更改导致另一个描述类型相关表达式的部分,它对我们的特定情况看起来很奇怪 .

    §17.6.2.2[temp.dep.expr](旧§14.6.2.2)¶4以下形式的表达式从不依赖于类型(因为表达式的类型不能依赖):...
    sizeof(type-id)
    ...

    还有关于依赖于值的表达式的部分,如果依赖于 type-idsizeof 可以是值依赖的 . 值依赖表达式与 decltype 之间没有关系 . 经过一番思考,我没有找到任何理由为什么 decltype(sizeof(T)) 一定不能或不能解析为 size_t . 而且我认为在编译器开发人员不太注意的标准中是非常偷偷摸摸的改变("involves a template parameter"到"type-dependent")(可能被许多其他变化所淹没,也许并不认为它可能实际上改变了某些东西,只是一个简单的配方改进) . 这种变化确实有意义,因为 sizeof(T) 不依赖于类型,它取决于值 . decltype(e) 的操作数是一个未评估的操作数,即不关心值,只关于类型 . 这就是 decltype 仅在 e 依赖于类型时返回唯一类型的原因 . sizeof(e) 可能只是依赖于 Value 的 .

    我用clang 5,gcc 8 -std=c++1z 尝试了代码 - 结果相同:错误 . 我走得更远,尝试了这段代码:

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

    给出了相同的错误,即使 sizeof(sizeof(T)) 既不依赖于类型也不依赖于值(参见this post) . 这让我有理由假设编译器以C-11/14标准(即"involves a template parameter")的旧方式工作,就像上面的clang 3.9源代码片段一样(我可以验证最新开发的clang 5.0具有相同的功能)行,没有找到与标准中的这个新变化相关的任何内容),但不依赖于类型 .

相关问题