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
的函数。假设我的enum
从0
变为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_cast
从 std::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&
的函数的值)。 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 引用类型?