constexpr overloading

constexpr overloading

本文关键字:overloading constexpr      更新时间:2023-10-16

相关:返回constexpr的函数无法编译

我觉得 constexpr 在 C++11 中的实用性有限,因为无法定义两个原本具有相同签名的函数,但其中一个是 constexpr,另一个不是 constexpr。换句话说,如果我能有一个,例如,一个只接受constexpr参数的constexpr std::string构造函数,以及一个非constexpr std::string构造函数,这将非常有帮助。另一个例子是一个理论上复杂的函数,可以通过使用状态来提高效率。你不能用 constexpr 函数轻松做到这一点,所以你有两个选择:有一个 constexpr 函数,如果你传入非 constexpr 参数,它会非常慢,或者完全放弃 constexpr(或者编写两个单独的函数,但你可能不知道要调用哪个版本(。

因此,我的问题是:

符合标准的 C++11 实现是否有可能允许基于 constexpr 参数的函数重载,或者这是否需要更新标准?如果不允许,是故意不允许的吗?


@NicolBolas:假设我有一个将enum映射到std::string的函数。假设我的enum0n - 1,最直接的方法是创建一个大小n填充结果的数组。

我可以创建一个static constexpr char const * []并在返回时构造一个std::string(每次调用函数时都要支付创建std::string对象的成本(,或者我可以创建一个static std::string const []并返回我查找的值,在我第一次调用函数时支付所有std::string构造函数的成本。似乎更好的解决方案是在编译时在内存中创建std::string(类似于现在使用 char const * 所做的(,但执行此操作的唯一方法是提醒构造函数它具有constexpr参数。

对于 std::string 构造函数以外的示例,我认为找到一个示例非常简单,如果您可以忽略constexpr的要求(从而创建一个非constexpr函数(,您可以创建一个更有效的函数。考虑这个线程:constexpr问题,为什么这两个不同的程序在g++上运行的时间如此不同?

如果我用constexpr参数调用fib,我不能比编译器完全优化函数调用做得更好。但是,如果我用非constexpr参数调用fib,我可能希望让它调用我自己的版本,该版本实现诸如记忆之类的东西(这需要状态(,这样我得到的运行时间类似于我的编译时间如果我传递了一个constexpr参数。

我同意缺少此功能 - 我也需要它。例:

double pow(double x, int n) {
    // calculate x to the power of n
    return ...
}
static inline double pow (double x, constexpr int n) {
    // a faster implementation is possible when n is a compile time constant
    return ...
}
double myfunction (double a, int b) {
    double x, y;
    x = pow(a, b);  // call version 1 unless b becomes a compile time constant by inlining
    y = pow(a, 5),  // call version 2
    return x + y;
}

现在我必须使用模板来执行此操作:

template <int n>
static inline double pow (double x) {
    // fast implementation of x ^ n, with n a compile time constant
    return ...
}

这很好,但我错过了超载的机会。如果我制作一个库函数供其他人使用,那么用户必须根据n是否是编译时常量使用不同的函数调用是不方便的,并且可能很难预测编译器是否已将n减少到编译时常数。

编辑:下面描述的技巧不再保证有效!

检测constexpr不能使用重载(就像其他人已经回复的那样(,但重载只是一种方法。

典型的问题是我们不能在函数constexpr中使用可以提高运行时性能的东西(例如调用非constexpr函数或缓存结果(。因此,我们最终可能会得到两种不同的算法,一种效率较低但可写constexpr,另一种经过优化以快速运行但不constexpr。然后,我们希望编译器不要为运行时值选择constexpr算法,反之亦然。

这可以通过检测constexpr并基于它"手动"进行选择,然后使用预处理器宏缩短接口来实现。

首先让我们有两个函数。通常,函数应该使用不同的算法达到相同的结果。我选择两种算法,它们在这里永远不会给出相同的答案,只是为了测试和说明这个想法:

#include <iostream>     // handy for test I/O
#include <type_traits>  // handy for dealing with types
// run-time "foo" is always ultimate answer
int foo_runtime(int)
{
    return 42;
}
// compile-time "foo" is factorial
constexpr int foo_compiletime(int num)
{
      return num > 1 ? foo_compiletime(num - 1) * num : 1;
}

然后我们需要一种检测该参数的方法是编译时常量表达式。如果我们不想像__builtin_constant_p那样使用特定于编译器的方式,那么也有方法可以在标准C++中检测到它。我很确定以下技巧是由约翰内斯·绍布发明的,但我找不到引用。非常好,清晰的技巧。

template<typename T> 
constexpr typename std::remove_reference<T>::type makeprval(T && t) 
{
    return t;
}
#define isprvalconstexpr(e) noexcept(makeprval(e))

noexcept运算符需要编译时工作,因此大多数编译器将优化基于它的分支。所以现在我们可以编写一个 "foo" 宏,它根据参数的一致性选择算法并对其进行测试:

#define foo(X) (isprvalconstexpr(X)?foo_compiletime(X):foo_runtime(X))
int main(int argc, char *argv[])
{
    int a = 1;
    const int b = 2;
    constexpr int c = 3;
    const int d = argc;
    std::cout << foo(a) << std::endl;
    std::cout << foo(b) << std::endl;
    std::cout << foo(c) << std::endl;
    std::cout << foo(d) << std::endl;
}

预期输出为:

42
2
6
42

在我尝试的几个编译器上,它的工作方式与预期一样。

它必须根据结果是否constexpr而不是参数来重载。

const std::string可以存储指向文本的指针,知道它永远不会被写入(使用 const_caststd::string中删除const是必要的,这已经是未定义的行为(。 只需要存储一个布尔标志来抑制在销毁期间释放缓冲区。

但是,非const字符串,即使从constexpr参数初始化,也需要动态分配,因为需要参数的可写副本,因此不应使用假设的constexpr构造函数。


从标准(第 7.1.6.1 节[dcl.type.cv](,修改const创建的任何对象都是未定义的行为:

除了可以修改任何声明为可变 (7.1.1( 的类成员之外,任何在其生存期 (3.8( 期间修改 const 对象的尝试都会导致未定义的行为。

虽然 C++11 中没有"constexpr 重载"这样的东西,但你仍然可以使用 GCC/Clang __builtin_constant_p内在的。请注意,这种优化对double pow(double)不是很有用,因为 GCC 和 Clang 都已经可以针对常数积分指数优化 pow,但是如果您编写多精度或向量库,那么这种优化应该有效。

检查此示例:

#define my_pow(a, b) (__builtin_constant_p(b) ? optimized_pow(a, b) : generic_pow(a, b))
double generic_pow(double a, double b);
__attribute__((always_inline)) inline double optimized_pow(double a, double b) {
    if (b == 0.0) return 1.0;
    if (b == 1.0) return a;
    if (b == 2.0) return a * a;
    if (b == 3.0) return a * a * a;
    if (b == 4.0) return a * a * a * a;
    return generic_pow(a, b);
}
double test(double a, double b) {
    double x = 2.0 + 2.0;
    return my_pow(a, x) + my_pow(a, b);
}

在此示例中,my_pow(a, x)将扩展到a*a*a*a(由于消除了死代码(,并且my_pow(a, b)将扩展到直接调用generic_pow而无需任何初步检查。

TL;DR:可以在 C++20 中使用,std::is_constant_evaluated<type_traits> 标头中。

我在 Tiib @Öö 在他的回答中提到的应用程序时遇到了这个问题,我想到了:我想在运行时使用更快的算法,但在编译时使用更慢(constexpr友好(的算法。

<小时 />

使用@Öö Tiib回答中的示例:

#include <iostream>
#include <type_traits>
constexpr int foo(int i) {
    if (std::is_constant_evaluated()) {
        // compile-time branch
        return (i > 1) ? foo(i - 1) * i : 1;
    } else {
        // runtime branch
        return 42;
    }
}
int main(int argc, char* argv[]) {
    int a = foo(1);
    const int b = foo(2);
    constexpr int c = foo(3);
    const int d = foo(argc);
    std::cout << a << std::endl;
    std::cout << b << std::endl;
    std::cout << c << std::endl;
    std::cout << d << std::endl;
}

给出输出

2
2
6
42

这是指向 godbolt 程序的链接。

请注意,foo确实是一个合法的constexpr函数,因为正如标准所说,(引用自cpp首选项(:

constexpr函数必须满足以下要求:

  • 至少存在一组参数值,因此函数的调用可以是核心常量表达式的计算子表达式(对于构造函数,在常量初始值设定项中使用就足够了(。
<小时 />

但是请注意,此程序:

#include <iostream>
#include <type_traits>
constexpr int foo(int i) {
    if (std::is_constant_evaluated()) {
        // compile-time branch
        return i > 1 ? foo(i - 1) * i : 1;
    } else {
        // runtime branch
        return 42;
    }
}
int main(int argc, char *argv[]) {
    int a = 1;
    const int b = 2;
    constexpr int c = 3;
    const int d = argc;
    std::cout << foo(a) << std::endl;
    std::cout << foo(b) << std::endl;
    std::cout << foo(c) << std::endl;
    std::cout << foo(d) << std::endl;
}

给出输出

42
42
42
42

(神螺栓链接(

我不完全确定为什么会发生这种情况,但我怀疑这是因为std::coutoperator<<没有标记为constexpr,所以所有对foo的函数调用都是在运行时发生的。然而,有趣的是,在 Godbolt 的程序集输出(对于 x86-64 GCC 11.2(中,我们可以看到内联的42。因此,该函数确实是在编译时计算的,只是不是我们最初期望的方式。

如前所述,这个问题感觉不对劲。


通过构造,std::string拥有内存。如果你想要对现有缓冲区的简单引用,你可以使用类似于llvm::StringRef的东西:

class StringRef {
public:
  constexpr StringRef(char const* d, size_t s): data(d), size(s) {}
private:
  char const* data;
  size_t size;
};

当然,令人遗憾的是strlen和所有其他C函数都不constexpr感觉像是标准的缺陷(想想所有的数学函数......


至于状态,你可以(一点(,只要你了解如何存储它。还记得循环等同于递归吗?好吧,同样,您可以通过将其作为参数传递给帮助程序函数来"存储"状态。

// potentially unsafe (non-limited)
constexpr int length(char const* c) {
  return *c == '' ? 0 : 1 + length(c+1);
}
// OR a safer version
constexpr int length_helper(char const* c, unsigned limit) {
  return *c == '' or limit <= 0 ? 0 : 1 + length_helper(c+1, limit-1);
}
constexpr int length256(char const* c) { return length_helper(c, 256); }

当然,这种形式的这种状态是有限的(你不能使用复杂的结构(,这是constexpr的限制。但这已经是一个巨大的飞跃。走得更远意味着更深入地了解纯洁(这在C++几乎是不可能的(。

符合标准的 C++11 实现是否有可能允许基于 constexpr 参数的函数重载,或者这是否需要更新标准?如果不允许,是故意不允许的吗?

如果标准没有说你可以做某事,那么允许某人做这件事就是非标准的行为。因此,允许它的编译器将实现语言扩展。

毕竟,这未必是坏事。但它不符合C++11。

我们只能猜测标准委员会的意图。他们可能是故意不允许的,或者这可能是一种疏忽。事实是,该标准不允许重载,因此不允许。

使用 SFINAE 检测编译时编译的另一个选项:http://coliru.stacked-crooked.com/a/f3a2c11bcccdb5bf

template<typename T>
auto f(const T&)
{
  return 1;
}
constexpr auto f(int)
{
  return 2;
}

////////////////////////////////////////////////////////////////////////
template<typename T, int=f(T{})>
constexpr bool is_f_constexpr_for(int) {return true;}
template<typename...>
constexpr bool is_f_constexpr_for(...) {return false;}

template<typename T>
auto g(const T& t)
{
  if constexpr (is_f_constexpr_for<T>(0))
  {
  }
  else
  {
  }
}

可以使用 Richard Smith 提出的基于缩小转换规则的方法来确定给定的静态存储变量是否为常量表达式。

我们可以在不缩小范围的情况下为unsigned int分配一个consexpr非负int

unsigned int u {std::max(0, -3)}; // compiles, max is constexpr

但是,如果我们使用变量,则无法执行上述操作:

int a = 3;
unsigned int u {std::max(0, a)}; // compilation error, narrowing int to unsigned int

为了确定给定int reference是否为常量表达式,我们可以测试它是否可以分配给unsigned int,而不会缩小其正值负值。对于在编译时已知的值的任何int,这应该是可能的,即可以被视为常量表达式。

template<const int& p> std::true_type
    is_constexpr_impl(decltype((unsigned int){std::max(-p, p)}));
template<const int& p> std::false_type
    is_constexpr_impl(...);
template<const int& p> using is_constexpr =
    decltype(is_constexpr_impl<p>(0));

现在,我们可以使用宏方法为运行时和编译时提供不同的实现:

int foo_runtime(int num) {
    return num;
}
constexpr int foo_compiletime(int num) {
      return num + 1;
}
#define foo(X) (is_constexpr<X>()?foo_compiletime(X):foo_runtime(X))

如前所述,它将模拟常量表达式的重载:

int main() {
    static int a = 3;
    static const int b = 42; // considered constexpr
    static const int c = foo_runtime(42); // not constexpr
    static constexpr int d = 4;
    static constexpr int e = -2;
    static int f = 0;
    static const int g = 0; // considered constexpr
    std::cout << foo(a) << std::endl;
    std::cout << foo(b) << std::endl;
    std::cout << foo(c) << std::endl;
    std::cout << foo(d) << std::endl;
    std::cout << foo(e) << std::endl;
    std::cout << foo(f) << std::endl;
    std::cout << foo(g) << std::endl;
}

上面很好,虽然不是很有用,因为它仅限于静态存储变量。但它确实存在基于constexpr的过载。


在不依赖于缩小转换范围的情况下实现相同目的的另一种方法是:

template<const int& p> std::true_type
    is_constexpr_impl(std::array<int, std::max(p, -p)>);
template<const int& p> std::false_type
    is_constexpr_impl(...);
template<const int& p> using is_constexpr = 
    decltype(is_constexpr_impl<p>(0));

使用上面的std::array取代了使用简单的 c-array,这种方法不适用于 gcc。


或者另一个 - 再次,不依赖于缩小规则 - 也可以正常工作:

template<const int& p, typename T = void>
struct is_constexpr: std::false_type {};
template<const int& p>
struct is_constexpr<p, std::void_t<int[std::max(p,-p)+1]>>: std::true_type {};

请注意,如果我们尝试使用更简单的方法实现相同的目标:

template<typename T>
struct is_constexpr: std::false_type {};
template<typename T>
struct is_constexpr<const T>: std::true_type {};
#define foo(X) (is_constexpr<decltype(X)>()?foo_compiletime(X):foo_runtime(X))

我们不会实现这条线的目标:

static const int c = foo_runtime(42); // const but not constexpr