可以在C++14 constexpr函数中使用for循环实例化模板

Possible to instantiate templates using a for loop in a C++14 constexpr function?

本文关键字:for 循环 实例化 C++14 constexpr 函数      更新时间:2023-10-16

我一直在摆弄clang的SVN构建,以试验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<i>()。在一个非constexpr函数中,我预计它将无法编译,因为您根本无法使用运行时int来生成模板的编译时实例化。但是,因为这是一个constexpr函数,所以我怀疑这是否可能。特别是,该值在编译时是已知的,即使允许它发生变异。

我知道下面的代码将编译

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

在SVN clang中,我的第一个例子不是:

test2.cpp:19:12:error:没有用于调用"foo"的匹配函数sum+=foo();^~~~~~test2.cpp:11:15:注意:忽略候选模板:显式指定无效模板参数"N"的参数constexpr int foo(){^

首先,我很难理解这个错误消息的第二部分。除此之外,它是C++14标准强制要求的吗?如果是,有人知道为什么不允许使用这种语法吗?

除此之外,它是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
}

这对你来说可能太晚了,但它可能对其他找到这个SO的人有用,所以这里是我的答案。

@Rapptz的答案没有错,但你的问题的答案可能是"是"。是的,for循环不能像讨论的那样使用。然而,您想要在循环上迭代,不必使用for循环心态,例如使用recursion是可能的。它更丑陋,但对于一些用户来说,从运行时(不应该在运行时)到编译时获得一些沉重的东西可能是值得的。对于一些人来说,增加的丑陋可能不值得牺牲代码的清洁度,所以这取决于每个人的决定,我只想证明这是可能的。资源受到严格限制的嵌入式领域可能值得考虑。有了这种递归,你可以描述许多算法,你可能会做尾部+头部的prolog,一次处理一个条目,或者复制数组,一次更改其中的一个条目(穷人以模板为中心的串联,而不改变返回类型的大小)。只是有一个限制,在编译时将看不到循环结束的"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)。您还必须在某个地方结束循环,因此您必须实现单独的退出情况。为了节省额外的键入,最好将出口大小写特例放在同一索引中,这样就不必创建重复的实现并增加维护。因此,你必须决定是递增计数还是递减计数更好。因此,您只需要实现一次组合的特殊情况和退出情况。为了把它放在上下文中,对于factorial之类的东西,我会递减,这样0大小写和循环结束将用相同的代码实现,然后让算法在从深度递归返回时完成工作。

如果你没有任何特殊情况,并且你必须做出一个特殊情况,例如在上面的代码中,我知道返回0是安全的,我知道你什么时候计数到10,但不包括它,所以我在索引10处做出了特殊情况,并返回0。

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

如果这个技巧对你来说不可能,那么你必须实现算法的一个子集(没有递归),但在索引9处停止,如下所示:

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

注意:您可以使用常量变量和方程式,只要它们在编译时即可。

完整示例:

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];
}