【问题标题】:Concepts causing compiler bugs in msvc and clang?在 msvc 和 clang 中导致编译器错误的概念?
【发布时间】:2023-03-15 05:07:01
【问题描述】:

我有以下代码:

#include <variant>

template <typename T>
struct S{};

using Var = std::variant<S<int>, S<float>>;

template <typename T>
concept VariantMember = requires(Var var) { std::get<T>(var); };

void foo(VariantMember auto x) {}
void foo(auto x) {}

void bar()
{
    foo(S<int>{});
    foo(S<char>{});
}

MSVC 错误

error C2338: get(variant&) 要求 T 在 Types 中只出现一次。 (N4835 [variant.get]/5)

在这种情况下,只有当我在变体 S 中使用同一模板的两个实例时才会发生这种情况。所以首先std::get出乎意料地出错,然后这个概念并没有失败,而是直接出错。有趣的是,如果我在概念之外使用std::get,例如:

std::get<S<int>>(Var{ S<int>{} });

代码编译良好。

Clang 似乎只是忽略了这个概念,在这两种情况下总是选择foo 的第一个重载。

godbolt

这些实际上是错误还是我搞砸了?

【问题讨论】:

标签: c++ visual-c++ clang c++20 c++-concepts


【解决方案1】:

首先,您的S 模板是噪音。删除它。

using Var = std::variant<int, float>;

template <typename T>
concept VariantMember = requires(Var var) { std::get<T>(var); };

template <int = 1>
void foo(VariantMember auto x) {}

template <int = 2>
void foo(auto x) {}

void bar()
{
    foo(int{});
    foo(char{});
}

我们得到同样的错误。如果我注释掉 char 的情况,它在 MSVC 中编译得很好。

std::get&lt;char&gt;(Var{}) 不需要对 SFINAE 友好。 std::get 可以(并且确实)在 MSVC 中失败。

在此版本中,一旦我删除了出现硬错误的 char,clang 和 msvc 都会调用 int=1 重载。

我们如何处理硬错误?写一个没有的getsafe_get:

template<class T, class...Ts>
requires (1 == (std::is_same_v<T, Ts>+...))
T& safe_get( std::variant<Ts...>& var ) 
  return std::get<T>(var);
}
template<class T, class...Ts>
requires (1 == (std::is_same_v<T, Ts>+...))
T const& safe_get( std::variant<Ts...> const& var ) 
  return std::get<T>(var);
}
// ...
template <typename T>
concept VariantMember = requires(Var var) {
  {safe_get<T>(var)};
};

(我添加并声明 1,这给了我“匹配且仅一次”)。

然后我们返回您的S 模板和everything works

【讨论】:

  • 我认为如果其中一个约束格式不正确,概念应该会失败。为什么它必须对 SFINAE 友好? This example 在概念子句中根本没有使用 SFINAE,它仍然按预期工作。
  • @Timo 在这里,std::get&lt;T&gt;(var) 格式正确,但实例化它不是。 MSVC 尝试实例化它(并生成一个硬错误),clang 没有(并认为它是有效的)。该标准绝对不会要求您完全实例化它(这将非常昂贵)。我不知道它是否允许,但我怀疑它会。请注意,调用方法是 SFINAE 友好的。 “SFINAE 友好”不是一个标准定义的术语,而是一种您可以在编译时安全地检查是否有效的表达式,并且错误发生“浅”,因此您可以检测并退出。
  • @Timo 您可以查看@Barry 的答案,了解使用更多技术语言的答案。 std::get 规定了关于它的论点的某些事情。如果您不符合要求,则您的程序格式不正确。它不约束它的论点。如果它限制了它的参数,重载就会失败。在这里,您以违反授权的方式使用std::get;您的程序格式不正确,无需诊断。
  • 谢谢,知道了!我现在明白为什么 MSVC 的行为如此,但为什么在我的示例中 clang 总是选择第一个重载?
  • @Timo 因为std::get&lt;any type&gt;(any variant) 是一个可供选择的有效重载。但是如果你选择它,它会使你的程序格式不正确。您强制只能使用有效类型调用它,而不限制只能使用有效类型。
【解决方案2】:

Yakk 解释了这个问题:std::get mandates 类型出现在变体中。它不是 SFINAE 友好的(即它不是一个约束)。因此,您的 VariantMember&lt;T&gt; 概念并没有真正检查任何内容:它要么是 true,要么是格式错误的。

相反,我们必须以不同的方式编写概念。对于Boost.Mp11,这是一个简短的单行字(一如既往):

template <typename T>
concept VariantMember = mp_contains<Var, T>::value;

我不清楚您是否需要检查 T 是否只是变体的成员(正如我在上面实现的那样),或者您是否需要检查 T 是否在变体中恰好存在一次。如果是后者,那只是一种不同的算法:

template <typename T>
concept VariantMember = mp_count<Var, T>::value == 1;

【讨论】:

  • 请注意,您需要 mp 包含 唯一 而不仅仅是包含。
  • @Yakk-AdamNevraumont 我不确定这是否一定是真的?
  • @Yakk-AdamNevraumont 当然可以,但我不知道目标是否是看看get 是否有效。
【解决方案3】:

与 Yakk 类似,我想出了自己的 get。我首先使用get 的原因是要找出类型T 是否是std::variant 的成员。所以我想我只是测试一下我是否可以在带有T 的变体上调用std::get

无论如何,如果有人发现它有用,这是我的解决方案:

namespace detail
{
    template <typename Test, typename Variant>
    struct CountVariantMembers;

    template <typename Test, template <typename ...> typename Variant, typename ...Args>
    struct CountVariantMembers<Test, Variant<Args...>>
    {
        static constexpr std::size_t Count = (std::same_as<Test, Args> + ...);
    };
}

template <typename T, typename Variant>
concept VariantMember = details::CountVariantMembers<T, Variant>::Count > 0;

【讨论】:

  • 你需要的 count 正好是 1;如果有两个ints,则不允许get&lt;int&gt;
  • @Yakk-AdamNevraumont 我只想知道T 是否是变体的成员。我不想打电话给get。我只需要重载解析的概念来防止隐式转换。
猜你喜欢
  • 2017-10-17
  • 2016-05-18
  • 1970-01-01
  • 2021-12-17
  • 2014-09-29
  • 2011-07-27
  • 2023-01-13
  • 2010-10-18
  • 2020-05-21
相关资源
最近更新 更多