constexpr 重载

Posted

技术标签:

【中文标题】constexpr 重载【英文标题】:constexpr overloading 【发布时间】:2012-02-14 17:33:28 【问题描述】:

相关:Function returning constexpr does not compile

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

因此,我的问题是:

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


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

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

对于std::string 构造函数以外的示例,我认为找到一个示例非常简单,如果您可以忽略constexpr 的要求(从而创建一个非constexpr 函数) ,您可以创建更高效​​的函数。考虑这个线程:constexpr question, why do these two different programs run in such a different amount of time with g++?

如果我用constexpr 参数调用fib,我无法比编译器完全优化函数调用做得更好。但是,如果我使用非constexpr 参数调用fib,我可能希望它调用我自己的版本,该版本实现了诸如memoization(需要状态)之类的东西,所以我得到的运行时间类似于我的编译我通过constexpr 参数的时间。

【问题讨论】:

你确定你真的需要这个吗?使用非常量参数调用 constexpr 函数是完全可以的。 引用this paper seems relevant to your question。 We don’t propose to make constexpr applicable to function arguments because it would be meaningless for non-inline functions (the argument would be a constant, but the function wouldn’t know which) and because it would lead to complications of the overloading rules (can I overload on constexpr-ness? — no). 我更新了我的问题以回复 Nicol Bolas。它还应该回答 Kerrek SB 提出的问题。 我也想要这个。另一个有用的例子是位字段的位/总体计数。许多处理器为此包含特殊指令,因此如果使用非 constexpr 参数调用 constexpr 函数,我想使用处理器指令。但是处理器指令在编译时不可用,所以我需要在编译时使用另一个算法。 @Adam:不。我正在写一篇针对 C++23 的论文:github.com/davidstone/isocpp/blob/master/… 【参考方案1】:

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

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 减少到编译时间常数与否。

【讨论】:

这是另一个示例:What is the function parameter equivalent of constexpr? 并尝试使用模板进行相同操作(真是一团糟):Parameterization and “function template partial specialization is not allowed”。【参考方案2】:

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

无法使用重载检测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++ 中也有方法可以检测到它。我很确定以下技巧是 Johannes Schaub 发明的,但我找不到引用。非常漂亮和清晰的技巧。

template<typename T> 
constexpr typename std::remove_reference<T>::type makeprval(T && t) 

    return t;


#define isprvalconstexpr(e) noexcept(makeprval(e))

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

#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 时,这似乎分崩离析。 constexpr int e = foo(c); //失败 @EdwardKMETT 它适用于您可以将foo_runtime() 写为constexpr 的情况。否则,是的,当我们需要返回常量表达式时,我们不能使用不返回它的函数。 IOW constexpr int fooB = foo_compiletime(b); C++ 中没有基于返回类型的重载。 C++17 unfortunately closed the door for this trick. @AmirKirsh 奇怪的是,他们也为 -std=C++14 模式删除了它。这闻起来像是阴谋让 constexpr 以任何方式都无法检测到。 constexpr 仍然可以检测到静态存储,请参阅:***.com/a/60714976/2085626【参考方案3】:

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

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

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


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

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

【讨论】:

const 不是类型;它是一个类型的修饰符。您的代码无法知道它在一个真正的const std::string 中,而不仅仅是一个std::string,而现在恰好被const 访问。没有这些知识,就无法区分差异,根本不可能实现这种优化。这就是为什么有人写了一个旧的boost::const_string 类;因为如果没有可以检测到它始终不变的专门代码,就无法实现它。 @NicolBolas:正确,但我不认为 Ben 是在说他的建议是可能的。允许将const 修饰符应用于构造函数会很有趣。 @NicolBolas:你倒退了。代码绝不能使用const_cast 来修改const std::string(显然是通过指针或引用),除非它有外部知识表明对象不是const。修改 const 创建的对象已经是未定义的行为,添加此优化不会破坏任何内容。我已经引用了标准的特定部分。 @BenVoigt:这并没有改变我的观点。 构造函数 无法知道它正在创建的对象是const 对象。没有它,就没有办法存储const 成员函数可以关闭的特殊数据,以知道该对象实际上是一个const 对象,而不仅仅是一个当前作为const 访问的对象(传递一个采用const&amp; 的函数的值)。 constexpr 构造函数不必创建 const 对象。您说的是一种全新的构造,它在 C++11 中不存在。 @NicolBolas:我认为我在回答的第一句话中恰当地涵盖了这一点。但是你仍然缺少一些东西。因为const 成员函数无法判断对象是否为const,所以它们必须假设它是并且不进行修改。唯一的问题是析构函数。【参考方案4】:

虽然 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,无需任何初步检查。

【讨论】:

很遗憾__builtin_constant_p 对 constexpr 函数参数不起作用。【参考方案5】:

问题,如上所述,感觉错误


一个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' ? 0 : 1 + length(c+1);


// OR a safer version
constexpr int length_helper(char const* c, unsigned limit) 
  return *c == '\0' or limit <= 0 ? 0 : 1 + length_helper(c+1, limit-1);


constexpr int length256(char const* c)  return length_helper(c, 256); 

当然,这种状态的这种形式有些限制(不能使用复杂的构造),这是constexpr 的限制。但这已经是一个巨大的飞跃。更进一步意味着更深入地了解纯度(这在 C++ 中几乎不可能)。

【讨论】:

【参考方案6】:

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

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

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

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

【讨论】:

SFINAE 可用于实现与默认规则不同的重载。在签名中使用函数参数作为模板参数可能是禁用非 constexpr 参数的函数的合法方法。或者可能不是。我不打算查,因为正如 Ben 解释的那样,结果无论如何都是无用的。【参考方案7】:

使用 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
  

  

【讨论】:

【参考方案8】:

可以识别给定的静态存储变量是否为常量表达式,使用一种基于缩小转换的方法proposed by 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 是否为 const 表达式,我们可以测试它是否可以分配给 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))

如前所述,it will mimic an overload for const expression:

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 的重载。


实现相同的另一种方法,不依赖于缩小转换,can be:

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 数组which doesn't work well for gcc with this approach。


或者另一个 - 再次,不依赖于缩小规则 - that also works fine:

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 ;

请注意,如果我们尝试使用 more simple approach 来达到同样的效果

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

【讨论】:

你能解释一下哪个版本是这里的“底线”吗?即推荐使用什么? @einpoklum 您需要什么用途? 暂时没有,只是好奇。 @einpoklum 的想法是提出可以有不同的方法检查is_constexpr,即使用:(1)缩小规则,(2)模板非类型参数,(3)数组大小.但是,所有这些选项仅适用于静态存储变量,因为它们都基于通过引用将被检查的变量作为模板参数传递。 一个比std::max(x, -x) 更简单的适用于所有类型(和INT_MIN)的实现是(void(expr), 0)。它总是产生值0,但只有当逗号运算符的第一个参数是一个常量表达式时,它才是一个常量表达式。

以上是关于constexpr 重载的主要内容,如果未能解决你的问题,请参考以下文章

为啥 NVCC 对 constexpr 比非 constexpr 主机函数更严格?

为啥这个 constexpr 静态成员函数在调用时不被视为 constexpr?

为啥从 constexpr 引用生成的汇编代码与 constexpr 指针不同?

为啥我不能使用 constexpr 全局变量来初始化 constexpr 引用类型?

在非 constexpr 函数上添加的 constexpr 限定符不会触发任何警告

以 constexpr 和不带 constexpr 的形式运行函数