【问题标题】:Why does this usage of C++17 if constexpr fail?如果 constexpr 失败,为什么 C++17 的这种用法会失败?
【发布时间】:2020-11-12 17:10:33
【问题描述】:

我正在尝试使用 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 constexprstd::enable_if 的替代品,而不是#if
  • 我最近做了一个quiz on this issue 和我note in my answer,它并不是要表现得像例如解除static_asserts

标签: c++ if-statement templates c++17 constexpr


【解决方案1】:

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

来自cppreference.com

在模板之外,完全检查丢弃的语句。 if constexpr 不能替代 #if 预处理指令:

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

知道如何跳过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@MSalters。正如他们所指出的,上述解决方案是格式错误的 NDR(因此,使用 MSVC compiler 编译没有任何意义,另一方面 GCC and clang at least catch this 通过提供一些编译器错误) 这已在@HolyBlackCat's 中详细说明, 回答!

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

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

这篇文章的好教训

  • 但是,您的程序是避免此类宏的一个很好的例子 和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
}

【讨论】:

  • 由于[temp.res]/8.1,您对if constexpr 的使用是格式错误的NDR。 GCC 和 Clang 似乎都拒绝它。
  • @HolyBlackCat:为什么选择 NDR?模板实例化。似乎有 4 个 T 可以实例化测试(std::nullptr_t,加上 cv-qualified。)
  • @HolyBlackCat 对,谢谢,这个讨论很有趣。看到这么多对错误答案的赞成票很有趣。我也没有检查就投票了,因为它看起来正确:p
  • @HolyBlackCat 好的,刚才我读完了。我现在意识到我的错误了。非常感谢您指出一个大错误,由于我的 MSVS,这在我看来也是正确的!让我更新我的答案!
  • @JeJo 从我所见,MSVC 往往是三个主要编译器中最不兼容的。 ://
【解决方案2】:

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

在模板之外,它就像普通的if 一样工作(除了它希望条件是constexpr)。

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

[temp.res]/8.1(强调我的)

可以在任何实例化之前检查模板的有效性。 ... 该程序格式错误,不需要诊断,如果:

— 不能为模板中的模板或 constexpr if 语句的子语句生成有效的特化,并且模板未实例化,...

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

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

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

可以让函数调用本身依赖于模板参数,但这可能是作弊,因为它需要遮蔽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)...);
    });
}

【讨论】:

  • 我认为你的回答是这是不可能的。我想我同意:) 我不得不说,我真的不喜欢“解决方案”。隐藏宏的名称有点绕开这个问题。
  • @cigien 是的,这可能是作弊。编辑使其更清晰。
【解决方案3】:

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

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

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

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

您可能需要重新考虑为什么要拥有一个扩展永远无效的宏。

【讨论】:

  • ... 这表明如果 constexpr 是一半的措施。如果像 D 一样,我希望我们可以有静态。
  • @einpoklum 是的,那很好。这就是我以前认为if constexpr 所做的:p
【解决方案4】:

如果可能,您可以修改 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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-06-09
    • 2020-05-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-13
    • 2011-02-01
    相关资源
    最近更新 更多