为什么C++编译器允许将函数声明为constexpr,但实际上并不是constexpr呢?

5

C++编译器为什么会允许声明一个函数为constexpr,而该函数却不能是constexpr呢?

例如:http://melpon.org/wandbox/permlink/AGwniRNRbfmXfj8r

#include <iostream>
#include <functional>
#include <numeric>
#include <initializer_list>

template<typename Functor, typename T, size_t N>
T constexpr reduce(Functor f, T(&arr)[N]) {
  return std::accumulate(std::next(std::begin(arr)), std::end(arr), *(std::begin(arr)), f);
}

template<typename Functor, typename T>
T constexpr reduce(Functor f, std::initializer_list<T> il) {
  return std::accumulate(std::next(il.begin()), il.end(), *(il.begin()), f);
}

template<typename Functor, typename T, typename... Ts>
T constexpr reduce(Functor f, T t1, Ts... ts) {
  return f(t1, reduce(f, std::initializer_list<T>({ts...})));
}

int constexpr constexpr_func() { return 2; }

template<int value>
void print_constexpr() { std::cout << value << std::endl; }

int main() {
  std::cout << reduce(std::plus<int>(), 1, 2, 3, 4, 5, 6, 7) << std::endl;  // 28
  std::cout << reduce(std::plus<int>(), {1, 2, 3, 4, 5, 6, 7}) << std::endl;// 28

  const int input[3] = {1, 2, 3};   // 6
  std::cout << reduce(std::plus<int>(), input) << std::endl;

  print_constexpr<5>(); // OK
  print_constexpr<constexpr_func()>();  // OK
  //print_constexpr<reduce(std::plus<int>(), {1, 2, 3, 4, 5, 6, 7})>(); // error 

  return 0;
}

输出:

28
28
6
5
2

为什么即使在C++14和C++1z中,下面这行代码://print_constexpr<reduce(std::plus<int>(), {1, 2, 3, 4, 5, 6, 7})>(); // error会出现错误?

为什么编译器允许将reduce()标记为constexpr,但即使在所有传递给reduce()的参数在编译时都已知,reduce()也不能用作模板参数?


一些编译器产生相同的效果 - 支持C++14的编译器-std=c++14:

对于以上所有情况,编译都可以通过,直到出现无用行://print_constexpr<reduce(std::plus<int>(), {1, 2, 3, 4, 5, 6, 7})>(); // error


3
C++编译器指的是哪个?GCC?Clang?MSVC?版本是多少? - Torbjörn
3个回答

7
为什么C++编译器允许将一个不可能成为constexpr的函数声明为constexpr?实际上并不是这样。你定义的是一个模板,而非一个constexpr函数。现在我们来具体解释一下:
struct Is_constexpr {
  constexpr Is_constexpr() = default;
  constexpr auto bar() {
    return 24;
  }
};

struct Not_constexpr {
  auto bar() {
    return 24;
  }
};

现在,如果您尝试将使用Not_constexpr的函数(而不是模板)定义为constexpr,则编译器将不允许:
constexpr auto foo_function(Not_constexpr v)
{
  return v.bar();
  // error: call to non-constexpr function 'auto Not_constexpr::bar()'
}

你正在定义一个模板。让我们看看这是如何工作的:
template <class T>
constexpr auto foo(T v)
{
  return v.bar();
}

编译器允许您这样做。没有错误。为什么?因为它是一个模板。一些实例可能是constexpr,一些不是,这取决于T
int main() {
  constexpr Is_constexpr is_c;
  constexpr Not_constexpr not_c;

  std::integral_constant<int, foo(is_c)> a; // OK
  //std::integral_constant<int, foo(not_c)> a; // ERROR

}

5
让我们直接从提案开始,www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2235.pdf中的第4.1节,第三段引用如下:
常量表达式函数可以使用非常量表达式进行调用,在这种情况下,不要求计算结果在编译时评估。
请参阅此问题:何时会对constexpr函数进行编译时评估?
template<typename Functor, typename T>
T constexpr reduce(Functor f, std::initializer_list<T> il) {
  return std::accumulate(std::next(il.begin()), il.end(), *(il.begin()), f);
}

正如您所知道的,std::accumulate不是一个constexpr函数。

template<int value>
void print_constexpr() { std::cout << value << std::endl; }

正如您所知,非类型模板参数必须是常量表达式


现在:

template<typename Functor, typename T>
T constexpr reduce(Functor f, std::initializer_list<T> il) {
  return std::accumulate(std::next(il.begin()), il.end(), *(il.begin()), f);
}

关于为什么它有效:以下是C++标准的说明:[dcl.constexpr/6](重点在于我):
如果类模板的成员函数或constexpr函数模板的实例化模板特化将无法满足constexpr函数的要求或constexpr构造函数,那么该特化仍然是constexpr函数或constexpr构造函数,即使不能在常量表达式中调用这样的函数...
注意:这里指出:
从函数模板实例化的函数称为函数模板特化;
当它不是一个模板时,它会失败:
int constexpr reduce(int(*f)(int, int), std::initializer_list<int> il) {
  return std::accumulate(std::next(il.begin()), il.end(), *(il.begin()), f);
}

编译器将会报错,因为你不能在一个被定义为constexpr的函数中调用非constexpr函数。

4
如果您编写了这段代码:
  constexpr int result = reduce(std::plus<int>(), {1, 2, 3, 4, 5, 6, 7});

您会发现reduce不会产生constexpr结果。
原因是因为“note: non-constexpr function 'accumulate >' cannot be used in a constant expression”,正如您在此处所见 - http://en.cppreference.com/w/cpp/algorithm/accumulate std::accumulate不是constexpr的。
编辑以回答实际问题,感谢@peterchen: 当它遇到使用情况时编译,它不会并且不能尝试解析函数,直到编译模板的特定版本。当它遇到使用情况并触发编译时,它解析累加并看到它不是constexpr,因此发出错误。

2
谢谢!是的,我知道。但为什么C++编译器允许将函数声明为constexpr reduce() - 如果它永远不可能是constexpr呢? - Alex
1
当代码使用时,它会被编译,直到编译特定版本的模板后才能尝试解析函数。当代码使用并触发编译时,它会解析累加器并发现它不是constexpr,因此会发出错误。 - 1stCLord
1
@TheSombreroKid:这是关键点——也许你应该在回复中加上它。 - peterchen
2
@Alex 换句话说,错误并非在声明模板时发生,而是在使用特定类型实例化模板时发生。std::accumulatereduce都可以针对所有使用的类型进行constexpr专门化。这些专门化可以出现在模板声明之后,即编译器可能尚未看到这些内容。 - peterchen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接