如果 constexpr 失败,为什么要使用 C++17?

Why does this usage of C++17 if constexpr fail?

我正在尝试使用 C++17 if constexpr 进行条件编译,但它的行为与我预期的不同。

例如,对于下面的代码,C++仍然编译由宏X2

定义的代码
#include <map>
#include <string>
#include <iostream>
#include <type_traits>

#define X1 pp("x")
#define X2 pp("x", "x")

void pp(const std::string str)
{
   std::cout << str << std::endl;
}

int main()
{
   std::map<std::string, int> map;

   if constexpr (std::is_null_pointer_v<decltype(map)>)
      X2;
   else
      X1;
}

并吐出此错误消息:

1.c:6:23: error: too many arguments to function ‘void pp(std::__cxx11::string)’
 #define X2 pp("x", "x")
                       ^
1.c:18:3: note: in expansion of macro ‘X2’
   X2;
   ^~

如何跳过X2的编译?

在模板之外,甚至 if constexpr 的错误分支也会被完全检查。一般来说,为此,需要

  • 要么使用 #if 预处理器指令,
  • 或将 if constexpr 代码放入模板中。

在您的情况下,您不能使用 #if,因为您的条件取决于预处理器不可用的类型信息。

此外,您不能使用 constexpr if,因为对于任何可能的模板参数,宏 X2 的扩展总是格式错误的。

您可能需要重新考虑为什么要使用扩展永远无效的宏。

这在模板之外是不可能的!

来自cppreference.com

Outside a template, a discarded statement is fully checked. if constexpr is not a substitute for the #if preprocessing directive:

void f() {
    if constexpr(false) {
        int i = 0;
        int *p = i; // Error even though in discarded statement
    }
}

Any idea how to skip compilation of X2?

一种选择是为此提供一个模板函数。
template<typename T>
void test()
{
   if constexpr (std::is_null_pointer_v<T>)
      X2;
   else
      X1;
}

int main()
{
   std::map<std::string, int> map;
   test<decltype(map)>();   // now chooses the X1
}

感谢 @HolyBlackCat and @MSalters. As they pointed out, the above solution is ill-formed NDR (therefore, compiling with MSVC compiler 没有任何意义,另一方面 GCC and clang at least catch this 通过提供一些编译器错误) 中有详细说明, 回答!

所以我们可以跳过X2的编译吗?

不幸的是,根据您的代码,!! preprocessor 将在翻译单元编译之前执行。 因此不能向 #if 指令提供类型信息(即 decltype(map))。 因此,对于您的情况,没有其他办法。

很好的教训 post:

  • 然而,您的程序是避免此类宏的一个很好的例子 和 constexpr if 混合。
  • 其次,通过不同的方式检查代码的正确性 编译器,如果可能的话!

我建议为 PP 重载一个函数(当然还有许多其他方法),这样您可以获得格式正确的代码:

See a demo.

#include <string>
#include <iostream>
#include <type_traits>
#include <map>

void pp(const std::string& str)
{
   std::cout << str << std::endl;
}

template<typename... T> void pp(const T&... args)
{
   // do something with args!
}

template<typename T>
constexpr void test()
{
   if constexpr (std::is_null_pointer_v<T>)
      pp("x", "x"); // call with args
   else
      pp("x"); // call with string
}

if constexpr并不是真正的“条件编译”。

在模板之外,它的工作方式与常规 if 相同(除了它希望条件为 constexpr)。

其他答案建议将它放在模板中(并使条件取决于模板参数),但这还不够。 (它似乎在 MSVC 中有效,但在 GCC 和 Clang 中无效。)那是因为:

[temp.res]/8.1 (emphasis mine)

The validity of a template may be checked prior to any instantiation. ... The program is ill-formed, no diagnostic required, if:

— no valid specialization can be generated for a template or a substatement of a constexpr if statement within a template and the template is not instantiated, ...

因此,如果您不能为 if constexpr 分支进行有效实例化(也就是说,如果对于所有可能的模板参数,该分支都是无效的),那么该程序就是格式错误的 NDR(这实际上意思是“无效,但编译器可能不够聪明,不会给你一个错误”)。

(正如@MSalters 所指出的,标准说“并且模板未实例化”,而不是“和模板 或 constexpr 的子语句 if 未实例化”。我认为这是一个有缺陷的措辞,因为它没有其他意义:似乎没有任何其他措辞来检查丢弃分支的有效性,因此它会使代码格式正确 仅当 封闭模板 实例化,否则 NDR 格式错误。参见 。)

似乎没有解决方法,也没有解决您的问题的好方法。

可以 使函数调用本身依赖于模板参数,但这可能是作弊,因为它需要隐藏 pp (或做 #define pp … ).

template <typename F>
void test(F pp) // Note parameter shadowing the global `pp` for the macros.
{
    std::map<std::string, int> map;

    if constexpr (std::is_null_pointer_v<decltype(map)>)
        X2;
    else
        X1;
}

int main()
{
    test([](auto &&... params)
    {
        pp(decltype(params)(params)...);
    });
}

如果可能,您可以修改 pp 以按照我认为的方式运行:

void pp(auto const &...args){
    ((std::cout << std::forward<decltype(args)>(args)), ...) << std::endl;
}

Link to compiler explorer

如果这不可能,那么我建议将 X2 修复为至少可以编译的代码。

如果无法更改 X2,那么我建议在调用 x2 时隐藏 pp,尽管这当然是最好的解决方案:

if constexpr (std::is_null_pointer_v<decltype(map)>) {
    auto pp = [] (auto const &...args){
        ((std::cout << std::forward<decltype(args)>(args)), ...) << std::endl;
    };
    X2;
} else {
    X1;
}

Link to compiler explorer