在constexpr函数内部,if和if constexpr有何区别?

4

最近我在我的constexpr函数中将一些if constexpr修改为if,发现它们仍然可以在编译时评估,并且运行良好。这里是一个最小的案例:

template<int N>
constexpr bool is_negative()
{
    if constexpr  (N >= 0) return false;
    else  return true; 
}
int main()
{
    constexpr  bool v = is_negative<1>();
}

实时演示

在上述情况中,N 必须在编译时已知,因为它是非类型模板参数,所以 if constexpr 在这里可以很好地工作。但是,它是一个 constexpr 函数,所以,如果我将 if constexpr 替换为 if,据我所知,仍然 可能 可以获得返回值:

template<int N>
constexpr bool is_negative()
{
    if  (N >= 0) return false;
    else  return true; 
}
int main()
{
    constexpr  bool v = is_negative<1>();
}

实时演示

根据 cpprefconstexpr 函数必须满足以下所有要求:不提及 if。因此,如果我理解正确的话,即使所有相关变量在编译时已知(如上面的 is_negative),包含 ifconstexpr 函数是否在编译时评估是实现定义行为。

所以,我的结论是:

  • c++17 之前,我们没有 if constexpr,所以选择 if,这意味着不能保证我们的 constexpr 函数在编译时被评估,所有取决于编译器实现
  • c++17 之后,如果我们希望 constexpr 函数在编译时被评估,则首选 if constexpr

以上都是我的个人想法,可能会遗漏/误解一些重要信息,请随时纠正。问题仍然未改变:ifif constexpr,对于预期在编译时评估的 constexpr 函数,应该选择哪个。

参考文献: - 什么是可用于 constexpr 函数的内容? - "if constexpr()" 与 "if()" 的区别


常量的评估一直是编译时评估。例如,使用模板is_negative()13使条件13 >= 0,编译器将解析为1(没有constexpr)。现代编译器优化整个条件,但我不知道这是否是强制性的,如果是,从哪个标准版本开始。顺便说一句,if (N >= 0) return true; else return false;:为什么不是return N >= 0;?还是因为简化了[mcve]? - Scheff's Cat
糟糕,我的错,这是不好的风格。我应该发布一个更好的示例... - Chen Li
好的,没问题。(你也可以说这是由于MCVE。这样的东西可能看起来有点愚蠢,但只是为了“展示原理”。在我看来,如果声明为这样做是可以接受的。);-) - Scheff's Cat
@Scheff 我发现我两个月前只告诉了一个操作员这是可怕的风格 https://stackoverflow.com/a/52826880/6949852 XD - Chen Li
我和 @Nelfeal 意见一致。使用常量进行算术或关系运算会再次得到常量(编译时评估)。我试图在 常量表达式 中找到相关部分,但失败了,直到我意识到 核心常量表达式 是“反向”表示的,即他们列出了任何使表达式 成为核心常量表达式的内容。我需要一点时间才能意识到这一点... ;-) - Scheff's Cat
2个回答

12
在c++17之前,我们没有if constexpr,所以选择的是if,这意味着不能保证我们的constexpr函数在编译时得到评估,所有这取决于编译器实现。
if语句不是constexpr并不意味着它不能作为constexpr表达式的一部分在编译时进行评估。在您的示例中,在两种情况下都在编译时评估了v,因为它需要是常量表达式。这不是实现定义的。
在c++17之后,如果我们希望constexpr函数在编译时得到评估,则更喜欢使用if constexpr。
引入constexpr if语句是为了解决问题。让constexpr函数在编译时得到评估并不是那个问题。
以下是一个示例,其中需要constexpr if而不是简单的if(取自cppreference):
template <typename T>
auto get_value(T t) {
    if constexpr(std::is_pointer_v<T>)
        return *t; // deduces return type to int for T = int*
    else
        return t;  // deduces return type to int for T = int
}

尝试移除 constexpr 关键字并观察发生了什么(demo)。

另外,请注意您始终可以使用其他方法来解决该问题,但是 if constexpr 具有简洁性的优势。例如,这是一个使用标签分派的等效 get_value

template<typename T>
auto get_value_impl(T t, std::true_type) {
    return *t;
}
template<typename T>
auto get_value_impl(T t, std::false_type) {
    return t;
}

template<typename T>
auto get_value(T t) {
    return get_value_impl(t, std::is_pointer<T>{});
}

演示


2

if constexpr和if的区别在于表达式是否总能在编译时执行。在您的示例中,您正在使用模板参数,因此您写哪个并不重要。如果您有以下代码,则可以注意到差异:

constexpr bool is_negative(int n)
{
    if  (n >= 0) return false;
    else  return true; 
}
int main(int argc, char**)
{
    constexpr  bool v = is_negative(1);
    bool b = is_negative(argc);
    return static_cast<int>(v || b);
}

对于上面的代码,写if constexpr是行不通的。因为你可以使用运行时值调用函数。

再次强调,这并不重要,因为两个代码路径都是有效的。当使用常量值调用函数时,正常的编译器优化应该会发挥作用。

if constexpr的真正意义在于只有一条路径有效时:

template <typename T>
constexpr auto get_value(T t) {
    if constexpr(std::is_pointer_v<T>)
        return *t; // deduces return type to int for T = int*
    else
        return t;  // deduces return type to int for T = int
}

如果T是一个int类型,那么使用*t的代码路径无效,因为你不能引用一个int类型。然而,因为使用了if constexpr代替if,所以当涉及到模板参数时,false路径上的代码只需要在语法上正确即可。

作为指南,编译器已经要求:当其中一个代码路径无效时,请使用if constexpr。当依赖于参数时,请使用if

对于if条件在编译时可以计算出两个有效路径的情况,请使用if constexpr,以便在调试构建中进行优化。如果你想在调试构建中逐步执行,则使用if

如果你走极端,表达式可能会变得太复杂,以至于编译器在生产构建中无法优化它,在这种情况下,在热路径上再次使用if constexpr可能会有趣。个人而言,我还没有遇到过这种情况,但我也没有用过它太多。


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