【问题标题】:Force template instantiation via typedef : success at g++ , fail at Visual C++通过 typedef 强制模板实例化:g++ 成功,Visual C++ 失败
【发布时间】:2019-09-16 09:07:44
【问题描述】:

我想强制模板实例化。
以下代码在 g++ (http://coliru.stacked-crooked.com/a/33986d0e0d320ad4) 上工作 (打印 1)。
但是,它在 Visual C++ (https://rextester.com/WGQG68063) 上打印错误的结果 (0)。

#include <iostream>
#include <string>
template <int& T>struct NonTypeParameter { };

//internal implementation
int lala=0;
template <typename T> struct Holder{
    static int init;
};
template <typename T> int Holder<T>::init = lala++;

//tool for user 
template <typename T> struct InitCRTP{ 
    using dummy=NonTypeParameter<Holder<T>::init>;
};

class WantInit : public InitCRTP<WantInit>{};//user register easily
int main(){
    std::cout << lala << std::endl;
}

它是 Visual C++ 编译器的错误,还是一种未定义的行为?
如果是 Visual C++ 错误,如何解决它(同时仍然保持美观)?

编辑:按照 Max Langhof(和许多人)的建议更改类 -> 结构。谢谢。

赏金原因

通过 StoryTellerMaxim Egorushkin 的相反解决方案以及他们的深入讨论(谢谢!),这听起来像是 C++ 规则的模糊区域。

如果是 Visual C++ 错误,我希望问题足够确定以报告。

此外,我仍然希望有一个很好的解决方法,因为这种技术对于自定义类型 ID 生成非常有用。显式实例化不是那么方便。

注意:我将赏金授予Kaenbyou Rin,因为对我来说,这很容易理解。
这并不意味着其余的答案不太正确或不太有用。
我仍然不确定哪个是正确的。读者应谨慎行事。
为了安全起见,我假设我只是不能使用该功能(目前)。感谢大家。

【问题讨论】:

  • 注意:"minimal reproducible example" 中的 "minimal" 并不代表 "使用尽可能少的空格和换行符" ;) 另外,请不要仅仅因为您希望它看起来像 Java 就按行执行 public: - 这绝对不是“美丽的”。考虑使用struct 而不是class,例如Holder 获取其成员的默认公开可见性。
  • 我个人认为Allman style 比所有将左大括号保持在同一行的变体更具可读性(因此更“漂亮”)。没有可用的官方统计数据,但这似乎是大多数人的意见,尤其是在 C++ 和 C 编码人员中。
  • Godbolt 链接为方便起见:godbolt.org/z/THyLe1
  • 显式使用类可能会给你想要的结果(需要自己测试,没有可用的MSVC)。您是否知道如果您在不同的编译单元中应用此模式,这些初始化发生的顺序是未指定的?因此,如果您只想为您的课程设置唯一的 id,那很好,如果您想要一些特定的顺序,那么您就出局了。
  • 一般建议:否定 id 有意义吗?如果不是,我希望unsigned int 明确表达这一点。

标签: c++ visual-c++ g++ c++17 template-instantiation


【解决方案1】:

肯定存在编译器错误。我们可以通过更改InitCRTP 来验证它:

template <typename T, typename = NonTypeParameter<Holder<T>::init>>
struct InitCRTP {
};

现在引用任何InitCRTP&lt;T&gt; 特化必须使用Holder&lt;T&gt;::init 来确定第二个模板参数。这反过来又会强制实例化Holder&lt;T&gt;::init,而VS doesn't instantiate that

一般来说,使用 CRTP 类作为基础应该已经实例化了类内的所有声明,包括 dummy 的声明。所以这也应该有效。

我们可以进一步验证。当用作基类时,成员函数的声明与类一起实例化:

template <typename T> struct InitCRTP{
    using dummy=NonTypeParameter<Holder<T>::init>;
    void dummy2(dummy);
};

不过,VC++ is stubborn。鉴于所有这些,以及 Clang 和 GCC 所表现出的行为,这是一个 VC++ 错误。

【讨论】:

  • 那个编译器错误是什么?
  • @MaximEgorushkin - 不实例化现在明显使用 ODR 的内容
  • Holder&lt;T&gt;::init ODR 在哪里使用?
  • @MaximEgorushkin - 如果你不能放手,请继续重复。无论哪种方式,标准is explicit 关于哪些定义未实例化,别名不在列表中。
  • 您应该发布错误报告并获得明确的答案。声称这是一个没有错误报告的错误是不可信的。
【解决方案2】:

class WantInit : public InitCRTP&lt;WantInit&gt; 既不实例化InitCRTP&lt;WantInit&gt;::dummy,也不实例化Holder&lt;WantInit&gt;::init,因为程序中实际上使用的东西没有引用它们。您代码中的隐式实例化链不需要实例化Holder&lt;T&gt;::init,请参阅implicit instantiation

这适用于类模板的成员:除非在程序中使用该成员,否则它不会被实例化,并且不需要定义。

解决方法是使用explicit template instantiation:

template struct Holder<void>;

这会导致 Holder&lt;void&gt; 与其所有非模板成员一起被实例化。

或者,您可以只实例化 Holder&lt;T&gt;::init 成员,例如:

static_cast<void>(Holder<void>::init);

IMO、gcc 和 clang 过于渴望实例化未引用的事物。这种行为不会破坏或拒绝有效代码,因此这几乎不是错误,但依赖于这种特定行为的副作用是脆弱且不可移植的。

【讨论】:

  • 这让我有点难过。
  • 根据stackoverflow.com/questions/24725152/…(只是一个例子,不是官方规定),它应该可以工作。
  • @javaLover @javaLover 您的代码中的隐式实例化链不需要实例化Holder&lt;T&gt;::init,这就是您引用的示例不适用的原因。
  • using dummy=NonTypeParameter&lt;Holder&lt;T&gt;::init&gt;;NonTypeParameter 采用 int&amp; 模板参数)是否不引用 Holder&lt;T&gt;::init
  • @javaLover IMO、gccclang 过于渴望实例化未提及的事物。这种行为不会拒绝有效代码,因此这几乎不是错误,但依赖于这种特定行为的副作用是脆弱的。
【解决方案3】:

让我们尝试绝对 ODR-使用 init 成员。

#include <iostream>
#include <string>

int lala=0;
template <typename T> struct Holder{
    static int init;
};
template <typename T> int Holder<T>::init = lala++;

template <typename T> struct InitCRTP{
    InitCRTP() { (void)Holder<T>::init; }
};

class WantInit : public InitCRTP<WantInit>{};
int main(){
    std::cout << lala << std::endl;
    // WantInit w;  <---------------------------- look here
}

现在,如果注释掉的行未注释,程序的结果会发生变化。恕我直言,任何东西的模板实例化状态或 ODR 使用状态不可能取决于是否调用了某些非模板函数(在这种情况下为 WantInit 构造函数)。我会说有一种相当强烈的臭虫气味。

【讨论】:

  • 有一个标准的投诉方法来检测模板是否被实例化。现在找不到链接...
【解决方案4】:

我相信@MaximEgorushkin 关于dummy 没有真正实例化这一事实是正确的。

dummy被声明了(因为它是一个类型别名declaration),为了声明这个别名,NonTypeParameter&lt;Holder&lt;T&gt;::init&gt;被声明了。为了声明NonTypeParameter&lt;Holder&lt;T&gt;::init&gt;,必须声明它的模板参数Holder&lt;T&gt;::init,因此Holder&lt;T&gt;也被声明了。

标准要求在实例化模板类时,定义其删除的成员函数。 [温度规格]

类模板特化的隐式实例化导致:[...]

——已删除成员函数、无作用域成员枚举、成员匿名联合定义的隐式实例化。

并且对void 的引用会导致编译错误。

我们可以用它来测试一个特定的模板是否是特化的。

#include <iostream>
#include <string>
template <int& T, typename U> struct NonTypeParameter { 
    U& f() = delete;
};

//internal implementation
int lala = 0;
template <typename T> struct Holder {
    T& f() = delete;
    static int init;
};
template <typename T> int Holder<T>::init = lala++;

//tool for user 
template <typename T> struct InitCRTP {
    using dummy = NonTypeParameter<Holder<T>::init, void>;
};

class WantInit : public InitCRTP<WantInit> {};//user register easily
int main() {
    std::cout << lala << std::endl;
}

此代码可以编译,因为 NonTypeParameter&lt;Holder&lt;T&gt;::init, void&gt; 仅被声明,未实例化。

但是如果我们把class WantInit : public InitCRTP&lt;WantInit&gt;改成

class WantInit : public InitCRTP<void>

在 MSVC、g++ 和 clang 中编译失败。

这是因为NonTypeParameter&lt;Holder&lt;void&gt;::init, void&gt;的声明需要Holder&lt;void&gt;的隐式实例化。

OP 遇到的问题完全是由于 MSVC 不知道 Holder&lt;T&gt;::init 被 ODR 使用:

#include <iostream>

template <int& T> struct NonTypeParameter { };

int lala = 0;

template <typename T> struct Holder {
    static int init;
};

template <typename T> int Holder<T>::init = lala++;

int main() {
    NonTypeParameter<Holder<int>::init> odr;
    std::cout << lala << std::endl;
}

MSVC 将输出0。这意味着它没有意识到 Holder&lt;int&gt;::init 已被 ODR 使用。

Compiler Explorer Link

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-03
    • 1970-01-01
    • 2021-02-17
    • 2011-12-20
    • 1970-01-01
    相关资源
    最近更新 更多