【问题标题】:Possible to instantiate templates using a for loop in a C++14 constexpr function?可以在 C++14 constexpr 函数中使用 for 循环实例化模板吗?
【发布时间】:2013-12-24 13:18:44
【问题描述】:

我一直在使用 SVN 构建的 clang 来试验 constexpr 的宽松规则。到目前为止,我还无法确定的一件事是,是否可以在编译时在 constexpr 函数中循环遍历元组内的元素。

因为我没有 C++14 兼容的标准库可供测试,所以我准备了以下等效测试:

template<int N>
constexpr int foo() {
  return N;
}

constexpr int getSum() {
  auto sum = 0;
  for (auto i = 0; i < 10; ++i) {
    sum += foo<i>();
  }
  return sum;
}

constexpr auto sum = getSum();

这里有趣的部分是foo&lt;i&gt;()。在非 constexpr 函数中,我希望它无法编译,因为您根本无法使用运行时 int 来生成模板的编译时实例化。因为这是一个constexpr 函数,所以我怀疑这是否可能。特别是,该值在编译时是已知的,即使它被允许改变。

我知道下面的代码编译:

constexpr auto nValue = 2;
foo<nValue>();

在 SVN clang 中,我的第一个示例没有:

test2.cpp:19:12: 错误: 没有匹配函数调用'foo' 总和 += foo(); ^~~~~~ test2.cpp:11:15:注意:候选模板被忽略:显式指定无效 模板参数“N”的参数 constexpr int foo() { ^

对于初学者,我很难解释此错误消息的第二部分。除此之外,它是由 C++14 标准规定的吗?如果是这样,有谁知道为什么不允许这种语法(简单的监督或防止某些事情发生)?

【问题讨论】:

  • 我认为错误是由于isum不是constexpr造成的。
  • @Jefffrey - 是的,我明白这一点,但这会使它们不可变,这在概念上似乎没有必要。此外,auto 不是关于打字方便,而是关于正确性、性能和可维护性。这些论点同样适用于原语。参考:herbsutter.com/2013/08/12/…
  • 模板在编译时被实例化,所以模板参数必须是常量表达式。它可能不是“概念上必要的”,但目前这就是指定语言的方式。拜托,我们不要为了本质上是风格问题而发起一场圣战。
  • @Mark 您正在寻找 C++11 标准中的第 5.19 节。它在 [expr.const]
  • @Jefffrey - 我给出了一般原因。这里是关于可维护性的,因为我不太可能引入转换。如果我决定从 int 到 long long,我可以。我最近不得不在一个项目中从 double 变为 long double,这被证明是最有用的。总的来说,这是一个好习惯,原语并没有那么特别,我们不要像对待它们一样对待它们。让我们现在也结束这一切 - 阅读 gotw,如果您同意,很好,如果不同意,让我们分道扬镳。

标签: c++ c++14 constexpr


【解决方案1】:

除此之外,它是由 C++14 标准规定的吗?如果是这样,有谁知道为什么不允许使用这种语法(简单的监督或防止某些事情发生)?

这是因为constexpr 不是编译时计算或使用所独有的。 constexpr 函数就是这样,允许在常量表达式中使用函数(或变量)。除此之外,它们是常规功能。在某些情况下(例如 static_assert 或数组大小等仅在编译时的情况下)恰好需要一个常量表达式。

您会注意到,您在代码中循环了一个变量,但您循环的变量本身不是 constexpr,因此它不是在 N 的模板实例化中使用的常量表达式.就目前而言,这与在 C++11 中执行此操作没有什么不同:

constexpr bool f(int x) {
    static_assert(x > 10, "..."); // invalid
    return true;
}

这显然是无效的,因为正如我之前提到的,您不必在独占编译时情况下使用constexpr 函数。例如,没有什么能阻止你这样做:

constexpr int times_ten(int x) {
    return x * 10;
}

int main() {
   int a = times_ten(20); // notice, not constexpr
   static_assert(times_ten(20) == 200, "...");
   static_assert(a == 200, "..."); // doesn't compile
}

【讨论】:

  • 这个论点是有道理的,但我已经发现只有当输入参数是 constexpr 时 constexpr 函数才会编译的情况。这也是因为主体中的代码,而不是函数签名;我不明白为什么这应该有任何不同。如果编译器知道该函数实际上是在编译时被评估的,则编译器可以(理论上)选择性地允许它。
  • @Mark 不幸的是,就语言而言,模板参数必须是常量表达式,而就语言而言,启用它的一种方法是constexpr,所以你循环的变量必须是constexpr
【解决方案2】:

这对您来说可能为时已晚,但它可能对其他找到此 SO 的人有用,因此这是我的答案。

@Rapptz 的答案没有错,但您的问题的答案可能是“是”。是的,不能像讨论的那样使用 for 循环。但是,您想遍历循环,不必具有 for 循环的心态,例如使用 recursion 是可能的。它更丑陋,但对于一些用户来说,从运行时(不应该在运行时)到编译时获得一些繁重的东西可能是值得的。对于一些增加的丑陋可能不值得牺牲代码的清洁度,所以这取决于每个人来决定,我只是想证明这是可能的。对资源有严格限制的嵌入式域可能值得考虑。通过这种递归,您可以描述许多算法,您可能可以像 prolog 一样执行 tail + head 并一次处理一个条目,或者复制数组并在其中更改一次条目(可怜的以模板为中心的串联而不改变返回类型)。只是有一个限制,在编译时不会看到循环结束的“if”条件。所以典型的算法是这样的:

template<int input>
int factorialTemplateSimpler() {
    if (input < 2) {
        return 1;
    } else {
        return factorialTemplateSimpler<input-1>() * input;
    }
}

不会编译,因为工具链会一直递减,直到它被终止(可能在 1000 次递归之后)。要让模板看到 结束状态,您必须像这样明确声明:

https://godbolt.org/z/d4aKMjqx3

template<int N>
constexpr int foo() {
  return N;
}

const int maxLoopIndex = 10;

template<int loopIndex>
constexpr int getSum() {
  return foo<loopIndex>() + getSum<loopIndex + 1>();
}

template<>
constexpr int getSum<maxLoopIndex>() {
  return 0;
}

int main() {
    constexpr auto sum = getSum<0>();
    return sum;
}

这是你想要的,它用 C++14 编译,不知道为什么它也用 C++11 编译。

许多算法都有一个特殊情况来做一些与典型算法不同的事情,你必须为它做一个单独的实现(因为 if 不会在实例化)。而且你还必须在某个地方结束循环,所以你必须实现单独的exit case。为了节省您额外的输入,最好将 exit casespecial case 放在同一个索引中,这样您就不必创建重复的实现并增加维护.因此,您必须决定以递增或递减方式计数是否更好。因此,您将只需要执行一次组合的特殊情况和退出情况。把它放到一个上下文中,对于像阶乘这样的东西,我会递减,所以循环的 0-case 和结束将用相同的代码实现,然后让算法在从深度递归返回时完成工作。

如果您没有任何特殊情况并且您必须制作一种特殊情况,例如在上面的代码中,我知道返回 0 是安全的,并且我知道您何时数到 10 但不包括它,因此我在索引 10 处做了特例并返回 0。

template<>
constexpr int getSum<maxLoopIndex>() {
  return 0;
}

如果您无法使用该技巧,那么您必须实现算法的一个子集(没有递归),但在索引 9 处停止,如下所示:

template<>
constexpr int getSum<maxLoopIndex-1>() {
  return foo<maxLoopIndex-1>();
}

注意:你可以使用 const 变量和方程,只要它们在编译时。

完整示例:

https://godbolt.org/z/eMc93MvW8

template<int N>
constexpr int foo() {
  return N;
}

const int maxLoopIndex = 10;

template<int loopIndex>
constexpr int getSum() {
  return foo<loopIndex>() + getSum<loopIndex + 1>();
}

template<>
constexpr int getSum<maxLoopIndex-1>() {
  return foo<maxLoopIndex-1>();
}

int main() {
    constexpr auto sum = getSum<0>();
    return sum;
}

这是一个递减的例子,让你更轻松(最终情况为 0):

https://godbolt.org/z/xfzcGMrcq

template<int N>
constexpr int foo() {
  return N;
}

template<int index>
constexpr int getSum() {
  return foo<index>() + getSum<index-1>();
}

template<>
constexpr int getSum<0>() {
  return foo<0>();
}

int main() {
    constexpr auto sum = getSum<10 - 1>();  // loop 0..9
    return sum;
}

如果您启用 C++20,那么我什至可以在编译时填充一个查找表,其中包含指向您的 foo 实例的函数指针,只是为了证明在编译时可以完成很多可能性。完整示例:

https://godbolt.org/z/c3febn36v

template<int N>
constexpr int foo() {
  return N;
}

const int lookupTableSize = 10;

template <int lookupIndex>
constexpr std::array<int(*)(), lookupTableSize> populateLookupTable() {
    auto result = populateLookupTable<lookupIndex - 1>();
    result[lookupIndex] = foo<lookupIndex>;

    return result;
}


template <>
constexpr std::array<int(*)(), lookupTableSize> populateLookupTable<0>() {
    std::array<int(*)(), lookupTableSize> lookupTable;
    lookupTable[0] = foo<0>;
    return lookupTable;
}


const auto lookupTable = populateLookupTable<lookupTableSize - 1>();

int main() {
    return lookupTable[2]();
}

这里的另一个例子是填充 cos 查找表:

https://godbolt.org/z/ETPvx4nex

#include <cmath>
#include <array>
#include <cstdint>
#include <algorithm>

const int lookupTableSize = 32;

template <int lookupIndex>
constexpr std::array<int8_t, lookupTableSize> populateLookupTable() {
    auto previousResult = populateLookupTable<lookupIndex + 1>();

    auto pi = acosf(-1);
    auto inRadians = (((float)lookupIndex)/lookupTableSize) * 2 * pi;
    previousResult[lookupIndex] = 127 * std::clamp(cosf(inRadians), -1.0f, 1.0f);

    return previousResult;
}


template <>
constexpr std::array<int8_t, lookupTableSize> populateLookupTable<lookupTableSize>() {
    return { 0 };
}


const auto lookupTable = populateLookupTable<0>();

int main() {
    return lookupTable[2];
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-03-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-09
    • 1970-01-01
    相关资源
    最近更新 更多