【问题标题】:Why isn't the compiler evaluating a constexpr function during compilation when every information is given?当给出所有信息时,为什么编译器在编译期间不评估 constexpr 函数?
【发布时间】:2015-09-01 10:35:39
【问题描述】:

这个

int main()
{
  std::cout << range(1, 11).reverse().sort().sum() << std::endl;
}

main 中的所有内容,正如代码所说,它创建一个从 1 到 10 的列表,将其反转,通过排序取消反转,并计算总和。因此输出应该是 55。

代码是一个实验,(ab)使用 C++14 中 constexpr 的宽松要求。我尽力创建了一个编译时列表类,但真的做得还不够。该类不完整,但仍然可以模仿很多函数式编程。

据我了解,constexpr 的要求是让编译器在编译时评估事物。所以我认为编译器可以简单地将所有内容替换为我的代码的常量 55,但事实并非如此。该代码确实具有在编译时获得结果所需的一切。我错过了什么?

在 cmets 中,我尝试使用 static_assert 中的结果来检查问题。 clang 和 gcc 都给我报错,但是我也没有看懂,而且后者好像坏了……

叮当声

a.cpp:142:17: error: static_assert expression is not an integral constant expression
  static_assert(range(1, 11).reverse().sort().sum() == 55, "");
                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
a.cpp:60:23: note: assignment to object outside its lifetime is not allowed in a constant
      expression
  l.array[l.length++] = t;
                      ^
a.cpp:134:9: note: in call to '&l->add(1)'
    l = l.add(a);
        ^
a.cpp:142:17: note: in call to 'range(1, 11, 1)'
  static_assert(range(1, 11).reverse().sort().sum() == 55, "");
                ^
1 error generated.

gcc

main.cpp: In function 'int main()':

main.cpp:142:3: error: non-constant condition for static assertion

   static_assert(range(1, 11).reverse().sort().sum() == 55, "");

   ^

main.cpp:142:38: error: 'constexpr List<T> List<T>::reverse() const [with T = int]' called in a constant expression

   static_assert(range(1, 11).reverse().sort().sum() == 55, "");

                                      ^

main.cpp:74:19: note: 'constexpr List<T> List<T>::reverse() const [with T = int]' is not usable as a constexpr function because:

 constexpr List<T> List<T>::reverse() const

                   ^

main.cpp:74:19: sorry, unimplemented: unexpected AST of kind result_decl

main.cpp:74: confused by earlier errors, bailing out

完整代码

#include <cstdint>
#include <iostream>
#include <limits>
#include <algorithm>
#include <initializer_list>

template<typename T>
class List
{
  template<typename T2>
  friend std::ostream &operator<<(std::ostream &, const List<T2> &);
public:
  constexpr List();
  constexpr List(std::initializer_list<T>);
  constexpr T head() const;
  constexpr List<T> tail() const;
  constexpr List<T> add(T) const;
  constexpr List<T> merge(List<T>) const;
  constexpr List<T> reverse() const;
  template<typename Filter>
  constexpr List<T> filter(Filter) const;
  constexpr List<T> sort() const;
  constexpr T sum() const;
private:
  int length;
  T array[0x100];
};

template<typename T>
constexpr List<T>::List()
: length(0)
{
}

template<typename T>
constexpr List<T>::List(std::initializer_list<T> l)
: length {static_cast<int>(l.size())}
{
  std::copy(l.begin(), l.end(), array);
}

template<typename T>
constexpr T List<T>::head() const
{
  return array[0];
}

template<typename T>
constexpr List<T> List<T>::tail() const
{
  List<T> l;
  l.length = length - 1;
  std::copy_n(array + 1, l.length, l.array);
  return l;
}

template<typename T>
constexpr List<T> List<T>::add(T t) const
{
  List<T> l {*this};
  l.array[l.length++] = t;
  return l;
}

template<typename T>
constexpr List<T> List<T>::merge(List<T> l) const
{
  std::copy_backward(l.array, l.array + l.length, l.array + l.length + length);
  std::copy_n(array, length, l.array);
  l.length += length;
  return l;
}

template<typename T>
constexpr List<T> List<T>::reverse() const
{
  List<T> l;
  l.length = length;
  std::reverse_copy(array, array + length, l.array);
  return l;
}

template<typename T>
template<typename Filter>
constexpr List<T> List<T>::filter(Filter f) const
{
  List<T> l;
  for (int i {0}; i < length; ++i)
  {
    if (f(array[i]))
    {
      l = l.add(array[i]);
    }
  }
  return l;
}

template<typename T>
constexpr List<T> List<T>::sort() const
{
  if (length == 0)
  {
    return *this;
  }
  return tail().filter([&](T t) {return t < head();}).sort().add(head())
  .merge(tail().filter([&](T t) {return t >= head();}).sort());
}

template<typename T>
constexpr T List<T>::sum() const
{
  if (length == 0)
  {
    return T {};
  }
  return head() + tail().sum();
}

template<typename T>
std::ostream &operator<<(std::ostream &os, const List<T> &l)
{
  os << '{';
  for (int i {0}; i < l.length - 1; ++i)
  {
    os << l.array[i] << ", ";
  }
  return os << l.array[l.length - 1] << '}';
}

inline constexpr List<int> range(int a, int b, int c = 1)
{
  List<int> l;
  while (a < b)
  {
    l = l.add(a);
    a += c;
  }
  return l;
}

int main()
{
  static_assert(range(1, 11).reverse().sort().sum(), "");
  std::cout << range(1, 11).reverse().sort().sum() << std::endl;
}

【问题讨论】:

  • 最好检查一下:static_assert(range(1, 11).reverse().sort().sum() == 55, "");,编译器会告诉你,哪一部分不能用在常量表达式中。
  • @ForEveR 我现在收到一条错误消息:note: assignment to object outside its lifetime is not allowed in a constant expression l.array[l.length++] = t;,你能帮我解释一下这是什么意思吗?
  • c++ 只说表达式必须能够在编译时计算。这并不意味着它是在编译期间完成的。这正是我为此使用 MTP 的原因。您是否尝试使结果成为编译时间常数的值,例如对于模板 int 参数常量?
  • gcc 给了我一个有趣的错误信息main.cpp:74:19: note: 'constexpr List&lt;T&gt; List&lt;T&gt;::reverse() const [with T = int]' is not usable as a constexpr function because: sorry, unimplemented: unexpected AST of kind result_decl confused by earlier errors, bailing out...

标签: c++ c++14 constexpr


【解决方案1】:

最初的错误是 array 没有在你的构造函数中初始化。你可以通过初始化来解决这个问题:

template<typename T>
constexpr List<T>::List()
: length(0)
, array{}
{
}

之后你会遇到你使用的&lt;algorithm&gt;函数不是constexpr的问题;您可以通过将示例定义从标准复制到您的实现中并标记您的副本constexpr 来解决此问题:

template<class BidirIt, class OutputIt>
constexpr OutputIt reverse_copy(BidirIt first, BidirIt last, OutputIt d_first)
{
    while (first != last) {
        *(d_first++) = *(--last);
    }
    return d_first;
}

// etcetera

最后,您使用 lambdas 作为过滤谓词(在 sort 中); lambda 在常量表达式中是非法的。这里的解决方法是手动将 lambda 扩展为函数对象:

template<typename T>
constexpr List<T> List<T>::sort() const
{
  if (length == 0)
  {
    return *this;
  }
  T pivot = head();
  struct Lt { T pivot; constexpr bool operator()(T t) const { return t < pivot; } };
  struct Ge { T pivot; constexpr bool operator()(T t) const { return t >= pivot; } };
  return tail().filter(Lt{pivot}).sort().add(pivot)
    .merge(tail().filter(Ge{pivot}).sort());
}

通过这些更改,您的代码将在 clang 3.7 下编译,但不是 gcc 5.2.0。


gcc 5.2.0 有两个错误:

首先,它不喜欢reverse_copy 中的组合递减间接*(--last);这很容易解决:

template<class BidirIt, class OutputIt>
constexpr OutputIt reverse_copy(BidirIt first, BidirIt last, OutputIt d_first)
{
    while (first != last) {
        --last;
        *(d_first++) = *last;
    }
    return d_first;
}

其次,它不喜欢将指针与array 进行比较;但是,可以通过将array + length 更改为&amp;array[length] 来满足。

Live example 进行了这些更改(适用于 clang 3.7 和 gcc 5.2.0)。

【讨论】:

  • 我实际上在尝试和你一样的东西,虽然卡在 lambda 部分:/
  • 有趣,看起来您通过用临时对象和分配替换大多数突变部分使其工作
【解决方案2】:

我尽力创建了一个编译时列表类

不过,这与编译时列表完全不同 - 值存储在数组中,并且在插入过程中长度会发生变化。编译时列表可以是非类型参数的可变参数模板,或类似 Boost.MPL Integral Sequence Wrapper 的东西,或 Loki 风格的非类型参数递归列表。

你尝试过 - ecatmur 成功 - 编写的是一个运行时列表,有时可以被 constexpr'd 删除。

特别是在您的代码中,List 成员不是常量; constexpr-runtime-list 解决方案是避免改变成员并跳过其他一些 constexpr 箍,而编译时列表解决方案是使 type 的长度(和值)属性。 p>


为了让您入门,这里有一个俗气且不完整的可变参数列表,以及您想要的一些算法:

template <int... Values> struct VList {};

template <int X, typename Xs> struct cat;
template <int X, int... Xs> struct cat<X, VList<Xs...>> {
    using result = VList<X, Xs...>;
};
template <typename Xs, int X> struct rcat;
template <int... Xs, int X> struct rcat<VList<Xs...>, X> {
    using result = VList<Xs..., X>;
};
template <typename L> struct reverse;
template <int X> struct reverse<VList<X>> { using result = VList<X>; };
template <int X, int Y> struct reverse<VList<X, Y>> { using result = VList<Y, X>; };
template <int X, int... Xs> struct reverse<VList<X, Xs...>> {
    using result = typename rcat<typename reverse<VList<Xs...>>::result, X>::result;
};
template <int From, int To> struct range;
template <int End> struct range <End,End> { using result = VList<End>; };
template <int From, int To> struct range {
    using result = typename cat<From, typename range<From+1,To>::result>::result;
};

int main()
{
  typename range<1,11>::result l;
  typename reverse<decltype(l)>::result r;
  return sizeof(l) + sizeof(r);
}

注意:

  • VList 只是一个类型包装器,它的内容无关紧要。我们同样可以使用std::tuple&lt;std::integral_constant&lt;int, ...&gt;...&gt;
  • range 应该断言 From&lt;=To 可以允许跨步等。
  • 我们真的不需要中间的reverse 专业化
  • cat 真的应该叫push_front 什么的,还有rcatpush_back。我以car 为前者开始了草图,并决定在上下文中可能会故意模糊。此外,这对递归版本更有意义。
  • X,Xs... 表示法基于 Haskell 的 x:xs 约定。当您将数据类型和算法转换为编译时操作时,函数式语言通常是寻找灵感的好地方。

【讨论】:

  • 如果是这样,那么constexpr的所有要求是什么?
  • 如果编译器可以在编译时确定所有相关值,那么它可以省略运行时计算。在您的情况下,它永远不可能,因为您编写了必须在运行时改变的运行时变量。编译器并不要求你的代码永远无法实现 constexpr-ness。
  • 如果您尝试声明您的类型的 constexpr 变量,那么编译器会告诉您它无法在编译时进行评估。
  • @Useless 您的声明似乎是错误的,请参阅所选答案。稍加调整后,完整的编译时评估似乎没有问题。
  • @xiver77 - 有趣,谢谢!这比我预期的编译器处理的要多。
【解决方案3】:

灵感来自:C++1y/C++14: Assignment to object outside its lifetime is not allowed in a constant expression?

List 类的私有成员不会在编译时初始化,因此它们只能在运行时存在,而不是在编译时存在。如果您进行更改以使它们阅读:

int length = 0;
T array[0x100] = {0};

你会更进一步地发现一个新的编译错误;)。另一种方法是在 constexpr 构造函数中正确初始化 array

template<typename T>
constexpr List<T>::List()
  : length(0),
    array({0})
{
}

尽管如 Useless 所述,但可能很难/根本不可能完成您想要完成的工作。

【讨论】:

    猜你喜欢
    • 2016-08-30
    • 2020-10-01
    • 1970-01-01
    • 2021-02-13
    • 2012-12-24
    • 2017-07-03
    • 1970-01-01
    • 2021-09-04
    • 1970-01-01
    相关资源
    最近更新 更多