【问题标题】:Why does removing the default parameter break this constexpr counter?为什么删除默认参数会破坏这个 constexpr 计数器?
【发布时间】:2017-12-16 16:39:33
【问题描述】:

考虑以下实现编译时间计数器的代码。

#include <iostream>

template<int>
struct Flag { friend constexpr int flag(Flag); };

template<int N>
struct Writer
{
    friend constexpr int flag(Flag<N>) { return 0; }
};

template<int N>
constexpr int reader(float, Flag<N>) { return N; }

template<int N, int = flag(Flag<N>{})>
constexpr int reader(int, Flag<N>, int value = reader(0, Flag<N + 1>{}))
{
    return value;
}

template<int N = reader(0, Flag<0>{}), int = sizeof(Writer<N>) >
constexpr int next() { return N; }


int main() {
    constexpr int a = next();
    constexpr int b = next();
    constexpr int c = next();
    constexpr int d = next();
    std::cout << a << b << c << d << '\n'; // 0123
}

对于第二个reader重载,如果我把默认参数放在函数体内,像这样:

template<int N, int = flag(Flag<N>{})>
constexpr int reader(int, Flag<N>)
{
    return reader(0, Flag<N + 1>{});
}

那么输出会变成:

0111

为什么会这样?是什么让第二个版本不再工作?

如果重要的话,我使用的是 Visual Studio 2015.2。

【问题讨论】:

  • CWG Issue #2118 将使这个程序在未来变得不正确。有关详细信息,请参阅this question
  • @Rakete1111 好的,我明白了。但是我还有一个问题,为什么不能省略变量“value”?

标签: c++ c++11 templates metaprogramming


【解决方案1】:

如果没有将value 作为参数传递,则不会阻止编译器缓存reader(0, Flag&lt;1&gt;) 的调用。

在这两种情况下,第一个 next() 调用将按预期工作,因为它会立即导致 SFINAEing 到 reader(float, Flag&lt;0&gt;)

第二个next() 将评估reader&lt;0,0&gt;(int, ...),它依赖于reader&lt;1&gt;(float, ...),如果它不依赖于value 参数,则可以缓存它。

不幸的是(具有讽刺意味的是)我发现的最好的确认constexpr 调用可以被缓存的来源是@MSalters 评论to this question

要检查您的特定编译器是否缓存/记忆,请考虑调用

constexpr int next_c() { return next(); }

而不是next()。在我的情况下(VS2017),输出变成0000

next() 受到保护,因为它的默认模板参数实际上随着每次实例化而改变,所以它每次都是一个新的单独函数。 next_c() 根本不是模板,所以可以缓存,reader&lt;1&gt;(float, ...) 也是。

我相信这不是一个错误,编译器可以合理地期望编译时上下文中的constexprs 是纯函数。

相反,应该将这段代码视为格式错误的代码 - 正如其他人所指出的那样,很快就会如此。

【讨论】:

  • 这是错误还是功能?
  • 你的意思是当'b'、'c'、'd'初始化时,它们只是运行相同的代码?
  • 我相信这是一种有效的行为。是的,我认为bcd 使用相同的代码进行初始化。我现在正在尝试扩展我的答案
  • 无关紧要。这是constexpr,内联仅适用于运行时行为。
  • 我认为正式术语是"memoizing",而不是缓存。但是请随意指责我“缓存”。
【解决方案2】:

value 的相关性在于它参与了重载决议。在 SFINAE 规则下,模板实例化错误静默将候选者排除在重载解决方案之外。但它确实实例化了Flag&lt;N+1&gt;,这会导致下一次重载决议变得可行(!)。所以实际上你正在计算成功的实例化。

为什么您的版本表现不同?您仍然引用Flag&lt;N+1&gt;,但在函数的实现中。这个很重要。对于函数模板,必须为 SFINAE 考虑 声明,但只有选择的重载才会被实例化。您的声明只是template&lt;int N, int = flag(Flag&lt;N&gt;{})&gt; constexpr int reader(int, Flag&lt;N&gt;);,并且依赖于Flag&lt;N+1&gt;

如 cmets 所述,不要指望这个计数器 ;)

【讨论】:

  • 对不起,我还是不太明白。您的意思是 var “value” 实例化“Flag”(结构)而不是“flag”(函数)?我认为调用“Next”实例化了“flag”(函数)。在我重写的“阅读器”函数中,“返回阅读器(0,Flag{});”这行不是吗?实例化 "Flag"?
  • @WangChu:糟糕,我的错。 (虽然同时使用flagFlag 不是最干净的方法恕我直言)。不带参数调用next() 需要使用其默认参数。该参数依赖于reader(0, Flag&lt;0&gt;{}),它是一个重载的函数调用表达式。正是这种重载决议触发了 SFINAE。
  • 对不起,我还是不明白...不管我是否重写reader函数,Next函数使用reader(0, Flag&lt;0&gt;{})触发SFINAE。但是如果我使用int value作为参数,它会触发递归;如果我省略int value,它不会触发递归,为什么?
  • @WangChu:看一下默认参数——int value = reader(0, Flag&lt;N + 1&gt;{})。由于您没有提供实际值(值是第三个参数,但您使用 2 个参数调用 reader),因此此重载具有可行数量的参数 if 可以实例化默认值。但这是相当循环的逻辑 - reader&lt;N&gt; 的默认值使用 reader&lt;N+1&gt; 如果已经实例化,否则使用 float 版本。
  • @Ap31:噢。我可能误读了那里的代码(这表明了这段代码的根本问题,我必须追溯编译器的步骤)。也就是说,将特定调用解析为 something 的事实正是 SFINAE 的重点。 reader(float, ...) 通常不会在重载决议中选择,因为它涉及从 0 (int) 到 0.0f (float) 的转换。当 SFINAE 静默排除 reader(int, ...) 时,float 重载是唯一剩下的选项。
猜你喜欢
  • 1970-01-01
  • 2013-02-03
  • 2016-08-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-23
  • 2021-05-27
  • 1970-01-01
相关资源
最近更新 更多