【问题标题】:constexpr overloadingconstexpr 重载
【发布时间】: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 questionWe 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/…

标签: c++ c++11 overloading compile-time-constant constexpr


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

【讨论】:

【解决方案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++ 中没有基于返回类型的重载。
  • @AmirKirsh 奇怪的是,他们也为 -std=C++14 模式删除了它。这闻起来像是阴谋让 constexpr 以任何方式都无法检测到。
  • constexpr 仍然可以检测到静态存储,请参阅:stackoverflow.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,但只有当逗号运算符的第一个参数是一个常量表达式时,它才是一个常量表达式。
      猜你喜欢
      • 2017-06-27
      • 1970-01-01
      • 2023-03-03
      • 2019-01-14
      • 1970-01-01
      • 2019-01-22
      • 2013-04-27
      • 2019-05-21
      相关资源
      最近更新 更多