【问题标题】:Concept resolve to the unexpected function template when using std::make_signed_t使用 std::make_signed_t 时对意外函数模板的概念解析
【发布时间】:2020-10-03 19:52:28
【问题描述】:

考虑以下sn-p

#include <type_traits>

template <typename T>
concept unsigned_integral = std::is_integral_v<T> &&std::is_unsigned_v<T>;

template <unsigned_integral T>
auto test(T) -> std::make_signed_t<T>; //(1)
template <typename T>
auto test(T) -> int; //(2)

int sandbox() {
  test(1u); // Call to (1) as expected
  test(1.0); // Expected to call (2), compilers choose (1) and fail to compile
}

MSVC 14.26、GCC-10 和 Clang-10 都无法编译这个,所以我想标准使它成为无效代码,所以这应该被认为是标准的疏忽吗?因为使用 SFINAE,代码按预期编译。

SFINAE 版本(这只适用于 double 案例,因为 unsigned int 案例会有歧义,但这不会影响我提出的问题)

template <typename T, typename = std::enable_if_t<unsigned_integral<T>>>
auto test(T) -> std::make_signed_t<T>;

编辑:显然,这与尾随返回类型无关,因此我已将标题更改为适当的标题。

【问题讨论】:

  • 您的等效 SFINAE 版本是什么?因为这很可能是这里的复杂点
  • 编译器不会选择 (1),他们只是考虑它,并在这样做的同时将 double 替换为 std::make_unsigned_t(由于某种原因,这对 SFINAE 不友好)。跨度>
  • @HolyBlackCat 但它应该作为 SFINAE 工作,因为如果第一次考虑失败,则应考虑下一个候选人,但事实并非如此
  • @StoryTeller-UnslanderMonica 我刚刚添加了 SFINAE 版本
  • @UyHà SFINAE 不会捕获所有错误。它只适用于所谓的即时上下文中的错误,其他错误仍然会中止编译。

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


【解决方案1】:

这是 CWG 2369(很遗憾,尽管多年前已提交,但并未在公开列表中)。我将在这里复制正文:

13.9.2[temp.deduct]第5段模板参数推导的规范规定处理顺序为:

  1. 在整个模板参数列表和类型中替换显式指定的模板参数;

  2. 从生成的函数签名中推导出模板参数;

  3. 检查非依赖参数是否可以从它们的参数中初始化;

  4. 将推导出的模板实参代入模板形参列表,尤其是任何需要的默认实参,以形成完整的模板实参列表;

  5. 在整个类型中替换生成的模板参数;

  6. 检查是否满足相关的约束;

  7. 检查剩余的参数是否可以从它们的参数中初始化。

这种排序在概念和 SFINAE 实现之间产生了意想不到的差异。例如:

template <typename T>
struct static_assert_integral {
  static_assert(std::is_integral_v<T>);
  using type = T;
};

struct fun {
  template <typename T,
    typename Requires = std::enable_if_t<std::is_integral_v<T>>>
    typename static_assert_integral<T>::type
  operator()(T) {}
};

这里利用替换顺序保证来防止static_assert_integral&lt;T&gt; 在不满足约束时被实例化。因此,以下断言成立:

static_assert(!std::is_invocable_v<fun, float>);

使用约束编写的此代码版本的行为意外不同:

struct fun {
  template <typename T>
    requires std::is_integral_v<T>
  typename static_assert_integral<T>::type
  operator()(T) {}
};

struct fun {
  template <typename T>
  typename static_assert_integral<T>::type
  operator()(T) requires std::is_integral_v<T> {}
};

static_assert(!std::is_invocable_v<fun, float>); // error: static assertion failed: std::is_integral_v<T> 

也许第 5 步和第 6 步应该互换。

这基本上与 OP 中的示例匹配。您认为您的约束阻止了 make_signed_t 的实例化(这需要一个整数类型),但实际上在检查约束之前它已被替换。

方向似乎是将上述步骤的顺序更改为 [1, 2, 4, 6, 3, 5, 7],这将使 OP 示例有效(一旦我们将 (1) 从考虑中删除在替换为make_signed_t) 之前使相关约束失效,这肯定是针对 C++20 的缺陷。但这还没有发生。

在那之前,您最好的选择可能是制作一个对 SFINAE 友好的 make_signed 版本:

template <typename T> struct my_make_signed { };
template <std::integral T> struct my_make_signed<T> { using type = std::make_signed_t<T>; };
template <typename T> using my_make_signed_t = /* no typename necessary */ my_make_signed<T>::type;

【讨论】:

  • 吓人!方向是执行第一组替换,然后进行约束检查,然后进行另一步替换......即使它更接近词汇顺序,也很难考虑。为什么不只使用词法顺序:在同一步骤中进行替换和约束检查。这将使约束位置变得重要,但我想大多数编码人员都预料到了这一点。 (我预料到了,问这个问题的人和其他人至少提出了 cmets)
  • @Oliv 我认为“检查约束,然后做事”比任何其他选择更合乎逻辑。词法顺序的问题是尾随的 requires 子句是您在某些情况下唯一可以放置约束的地方,我不希望那些有不同的规则 - 我真的是指要约束的那些约束!
  • 如果可以实现的话,这对理论家来说会很棒。但是之前必须执行一些替换。因此,所提出的解决方案并没有提供理想的解决方案,但它增加了复杂性。再看看上面的评论,有一些非常熟练的人期望一次性执行约束和替换。声明是一个语义原子,不,在任何替代方案中都没有更多的逻辑意义。即使在形式逻辑中,我们也使用左右自上而下的顺序。我认为该决定应该针对不太令人惊讶的选项。
  • @Oliv 我看不出它是如何增加复杂性的。它以不同的顺序执行相同的步骤。
  • 根据我读到的template &lt;integral T, class U = make_signed_t &lt;T&gt;&gt; auto test (T) 会遇到同样的问题,因为模板头中的模板替换将在约束检查之前执行。但是template &lt;integral T&gt; make_signed_t &lt;T&gt; test(T) 的问题将得到解决。
【解决方案2】:

根据[meta]make_signed 要求模板参数是整数类型:

要求:T 是除 cv bool 以外的整数或枚举类型。

所以make_signed 对 SFINAE 不友好。

在模板参数替换后执行约束完成检查。模板参数替换发生在建立重载候选集和约束完成检查时,确定哪些重载候选是可行的。

以你的情况为例:

  1. 编译器建立重载候选集,这里不检查约束。那么编译器要使用的就等价于:

    template <class T>
    auto test(T) -> std::make_signed_t<T>; //(1)
    template <typename T>
    auto test(T) -> int; //(2)
    

编译器将T 推导出为double,它将T 替换为make_signed_t => 错误:在test 声明的直接上下文中不会发生替换失败。

编译器在此停止,编译未达到选择可行候选者的第二步,其中将检查约束。

【讨论】:

  • 嗯。我想知道一次性检查要求和常规 SFINAE 有什么问题。
  • 写一个更“SFINAE 友好”的version of std::make_signed 来解决问题会被认为是不好的做法吗?
  • 这肯定令人惊讶,我认为概念解析机制会类似于 SFINAE 但显然不是
猜你喜欢
  • 2022-09-29
  • 2012-01-13
  • 2018-12-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-19
  • 1970-01-01
相关资源
最近更新 更多