使用 SFINAE 定义一个要求函数不存在的函数是否可以?

2024-02-15

这个问题中的代码基于这个答案 https://stackoverflow.com/a/73307583/4117728。我有点困惑它是如何产生输出的,以及它是否都定义良好

#include <type_traits>
#include <iostream>
#include <vector>

struct bar {};
void foo(bar) {}
struct moo {};

template<class T>
struct is_fooable {
    static std::false_type test(...);
    
    template<class U>
    static auto test(const U& u) -> decltype(foo(u), std::true_type{});
    static constexpr bool value = decltype(test(std::declval<T>()))::value;
};
template<class T> inline constexpr bool is_fooable_v = is_fooable<T>::value;

template <typename T>
std::enable_if_t<!is_fooable_v<T>,void> foo(T) {}

int main() {
    std::cout << is_fooable_v<bar>;
    std::cout << is_fooable_v<moo>;
    foo(bar{});
    foo(moo{});   
}

使用 gcc 输出 https://godbolt.org/z/scYEqdfjE(与 clang 和 msvc 相同):

10

If is_fooable_v<moo> is false那么 SFINAE 不会丢弃foo模板然后moo虽然是“fooable”is_fooable_v<moo> is false尽管如此。

我觉得令人困惑的是,该特征的用途有限,因为它无法判断是否moo在它被用来定义之后是“fooable”foo<T> with T==moo。不管潜在的混乱如何,代码是否定义良好?

可以根据测试函数是否存在的特征来定义函数吗?


tl;dr

  • 这个模式是明确定义的
  • std::enable_if_t<!is_fooable_v<T>,void> foo(T) is visible during the initialization of is_fooable<T>::value
    • 但模板参数替换会失败,所以is_fooable<T>::valuefalse
  • 您可以使用第二个特征类来检测这两个函数(例如struct is_really_fooable具有相同的定义is_fooable)

1. 免责声明

本文仅考虑 C++20 标准。
我没有检查以前的标准是否符合。

2. 模板的可见性foo功能

模板化 foo 函数 (template <typename T> std::enable_if_t<!is_fooable_v<T>,void> foo(T) {}) 从内部可见is_fooable并参与重载决策。

这是因为test(std::declval<T>())依赖于T- 所以名称查找需要考虑模板定义的上下文and实例化点的上下文:

13.8.2 从属名称 [temp.dep] (2) https://timsong-cpp.github.io/cppwp/n4861/temp.res#temp.dep-2
如果运算符的操作数是依赖于类型的表达式,则该运算符还表示依赖名称。
[ Note:这些名称是未绑定的,并在模板实例化时查找([温度点] https://timsong-cpp.github.io/cppwp/n4861/temp.res#temp.point)在模板定义的上下文和实例化点的上下文中([临时候选人] https://timsong-cpp.github.io/cppwp/n4861/temp.res#temp.dep.candidate). — end note ]

// [...]

template<class T>
struct is_fooable { // <-- Template definition
    static std::false_type test(...);
    
    template<class U>
    static auto test(const U& u) -> decltype(foo(u), std::true_type{});
    static constexpr bool value = decltype(test(std::declval<T>()))::value;
};

// is_fooable is dependent on T in this case,
// so the point of instantiation will be the point where is_fooable_v<T> is itself instantiated
template<class T> inline constexpr bool is_fooable_v = is_fooable<T>::value;

template <typename T>
// same as for is_fooable_v - 
std::enable_if_t<!is_fooable_v<T>,void> foo(T) {}

int main() {
    std::cout << is_fooable_v<bar>; // <-- Point of instantiation for is_fooable<bar>
    std::cout << is_fooable_v<moo>; // <-- Point of instantiation for is_fooable<moo>
    foo(bar{});
    foo(moo{});
}

所以模板化的foo函数从模板定义中不可见,但从实例化点来看它是可见的 - 并且由于我们需要同时查看两者,因此将考虑在其中进行重载解析is_fooable.

注意:如果表达式不依赖于模板参数,例如foo(12),那么我们只需要考虑模板定义的上下文:

13.8 名称解析[temp.res] (10) https://timsong-cpp.github.io/cppwp/n4861/temp.res#10
如果一个名字不依赖于模板参数 https://timsong-cpp.github.io/cppwp/n4861/temp.param#nt:template-parameter(如定义[温度差异] https://timsong-cpp.github.io/cppwp/n4861/temp.res#temp.dep),该名称的声明(或声明集)应位于该名称出现在模板定义中的范围内;该名称绑定到在该点找到的声明(或多个声明),并且此绑定不受实例化点可见的声明的影响。

13.8.5.1 实例化点 [temp.point] (7) https://timsong-cpp.github.io/cppwp/n4868/temp.point#7不适用于这种情况 - 我们只有一个翻译单元,并且每个翻译单元只有一个实例化点is_fooable<T>- 这样就不会违反网上解决规则。

注意:如果您在多个翻译单元中使用它,您仍然需要小心(但这基本上适用于任何类似特征的模板)。 例如这将违反 ODR 规则(格式错误,ndr):

// Translation unit 1
struct bar{};
void foo(bar) {}

template<class T> struct is_fooable { /* ... */ };

// would print 1
void test1() { std::cout << is_fooable<bar>::value << std::endl; }

// Translation unit 2
struct bar{};
// foo(bar) not defined before test2

template<class T> struct is_fooable { /* ... */ };

// would print 0
void test2() { std::cout << is_fooable<bar>::value << std::endl; }

// -> different definitions of is_fooable<bar>::value in different translation units
// -> ill-formed, ndr

3. How is_fooable<moo>::value最终成为false

本质上,这是常量表达式与 SFINAE 结合的有趣应用。

首先我们需要介绍一些基本规则:

  • 在变量自身初始化期间访问变量是未定义的行为。 (例如。int x = x;)
    这是由于以下两条规则:(强调我的)

    6.7.3 生命周期[basic.life] (1) https://timsong-cpp.github.io/cppwp/n4861/basic.life#1
    [...] T 类型对象的生命周期从以下时间开始:

    • 获得类型 T 具有适当对齐和大小的存储,并且
    • 其初始化(如果有)已完成 [...]

    6.7.3 生命周期[basic.life] (7) https://timsong-cpp.github.io/cppwp/n4861/basic.life#7
    [...]在对象的生命周期开始之前但在分配该对象将占用的存储空间之后[...],可以使用任何引用原始对象的泛左值,但只能以有限的方式使用。 [...] 如果出现以下情况,则程序具有未定义的行为:

    • 左值用于访问对象 [...]
  • 非类型模板参数必须是转换后的常量表达式

    13.4.2 模板非类型参数[temp.arg.nontype] (2) https://timsong-cpp.github.io/cppwp/n4861/temp.arg.nontype#2
    A 模板参数 https://timsong-cpp.github.io/cppwp/n4861/temp.names#nt:template-argument对于非类型模板参数 https://timsong-cpp.github.io/cppwp/n4861/temp.param#nt:template-parameter应为转换后的常量表达式 ([表达式.const] https://timsong-cpp.github.io/cppwp/n4861/expr.const)的类型模板参数 https://timsong-cpp.github.io/cppwp/n4861/temp.param#nt:template-parameter.

    • A converted constant expression must be a constant expression

      7.7 常量表达式[expr.const] (10) https://timsong-cpp.github.io/cppwp/n4861/expr.const#10
      A 转换后的常量表达式T 类型是一个表达式,隐式转换为 T 类型,其中转换后的表达式是常量表达式 [...]

    • A constant expression is a core constant expression:

      7.7 常量表达式[expr.const] (11) https://timsong-cpp.github.io/cppwp/n4861/expr.const#11
      A 常量表达式是泛左值核心常量表达式,它引用作为常量表达式(如下定义)允许的结果的实体,或者是纯右值核心常量表达式 [...]

    • 因此,要包装它,非类型模板参数必须具有一个核心常量表达式的值(忽略转换部分)

现在我们可以把它拼凑起来:

  • 让我们从std::cout << is_fooable_v<moo>;:这将实例化is_fooable_v<moo>,这又会实例化is_fooable<moo>::value.
  • So the initialization of is_fooable<moo>::value begins.
    • The overload resolution for test() takes place with both test functions as candidates
      • test(...)很简单,并且始终是一个可行的功能(优先级较低)
      • test(const U& u) would be viable and will be instanciated
        • this in turn will result in the overload resolution of foo(u), which also has 2 potential candidate functions: foo(bar) and foo(T)
          • foo(bar)是不可行的,因为moo不可转换为bar
          • foo(T) would be viable and will be instanciated
            • 在参数替换为foo(T)我们会遇到一个问题:foo(T)访问is_fooable<moo>::value- 尚未初始化(我们当前正在尝试初始化它)
            • this would be undefined behaviour normally - but because we're in a constantly evaluated context (non-type template arguments like the one of std::enable_if_t need to be converted constant expressions) a special rule applies: (emphasis mine)

              7.7 常量表达式[expr.const] (5) https://timsong-cpp.github.io/cppwp/n4861/expr.const#5
              表达式 E 是核心常量表达式除非 E 的求值遵循抽象机的规则([执行简介] https://timsong-cpp.github.io/cppwp/n4861/intro.execution),将评估以下其中一项:
              [...]

              • 具有未定义行为的操作,如[intro] https://timsong-cpp.github.io/cppwp/n4861/intro通过[cpp] https://timsong-cpp.github.io/cppwp/n4861/cpp本文件的 [ Note:例如,包括有符号整数溢出([expr.prop] https://timsong-cpp.github.io/cppwp/n4861/expr.prop),某些指针算术([表达式.添加] https://timsong-cpp.github.io/cppwp/n4861/expr.add), 被零除 https://timsong-cpp.github.io/cppwp/n4861/expr.mul,或某些轮班操作 https://timsong-cpp.github.io/cppwp/n4861/expr.shiftend note]; [...]
              • 6.7.3 生命周期[basic.life] https://timsong-cpp.github.io/cppwp/n4861/basic.life#1在。。。之间4 简介[简介] https://timsong-cpp.github.io/cppwp/n4861/intro and 15 预处理指令 [cpp] https://timsong-cpp.github.io/cppwp/n4861/cpp因此该规则适用于访问其生命周期之外的变量。
              • 因此is_fooable_v<T> within std::enable_if_t<!is_fooable_v<T>,void> is 不是核心常量表达式,该标准要求非类型模板参数
              • 所以这个实例化foo(T)将是格式不正确的(并且不是未定义的行为)
            • 所以模板参数替换为foo(T)失败并且不会成为可行的功能
          • 没有可行的功能foo(u)可以匹配
        • 模板参数替换为U in test(const U& u)由于没有可以调用的可行函数而失败foo(u)
      • test(const U& u)不再可行,因为foo(u)格式不正确 - 但是test(...)仍然可行
      • test(...)将是最好的可行函数(并且错误来自test(const U& u)将因 SFINAE 被吞食)
    • test(...)在重载决策期间选择的,所以is_fooable<moo>::value将被初始化为false
  • 的初始化is_fooable<moo>::value做完了

因此,这是完全符合标准的,因为常量表达式中不允许未定义的行为(因此foo(T)在初始化期间总是会导致替换失败is_fooable<T>::value)

这全部包含在is_fooablestruct,所以即使你第一次调用foo(moo{});你会得到相同的结果,例如:

int main() {
  foo(moo{});
  std::cout << is_fooable_v<moo>; // will still be false
}

它本质上与上面的顺序相同,只是你从函数开始foo(T),然后导致实例化is_fooable_v<T>.

  • (有关事件发生的顺序,请参阅上文)
  • is_fooable_v<T>被初始化为false
  • 的参数替换foo(T)成功 ->foo<moo>(moo{})将被称为

注意:如果您注释掉test(...)函数(因此 SFINAE 将无法抑制替换失败test(const U& u))那么你的编译器应该报告这个替换错误(它的格式不正确,因此应该有一条诊断消息)。
这是 gcc 12.1 的结果:(仅有趣的部分)
godbolt https://godbolt.org/z/3rz9d4s7j

In instantiation of 'constexpr const bool is_fooable<moo>::value':
error: no matching function for call to 'is_fooable<moo>::test(moo)'
error: no matching function for call to 'foo(const moo&)'
note:  candidate: 'template<class T> std::enable_if_t<(! is_fooable_v<T>), void> foo(T)'
note:  template argument deduction/substitution failed:
error: the value of 'is_fooable_v<moo>' is not usable in a constant expression
note:  'is_fooable_v<moo>' used in its own initializer
note:  in template argument for type 'bool'

四、备注

你可以缩短你的is_fooable如果您使用 C++20 需要子句,则具有特征,例如:

template<class T>
constexpr bool is_fooable_v = requires(T const& t) { foo(t); };

请注意,您can't使用概念,因为概念永远不会被实例化。

如果您还想能够检测到foo(T)您可以通过定义第二个特征来做到这一点。
第二个特征不会参与初始化恶作剧is_fooable使用,因此能够检测到foo(T)超载:godbolt https://godbolt.org/z/nshzvP4fj

struct bar {};
void foo(bar) {}
struct moo {};

template<class T>
constexpr bool is_fooable_v = requires(T const& t) { foo(t); };

template<class T>
constexpr bool is_really_fooable_v = requires(T const& t) { foo(t); };

template <typename T>
std::enable_if_t<!is_fooable_v<T>,void> foo(T) {}

int main() {
    foo(moo{});
    std::cout << is_fooable_v<moo>; // 0
    std::cout << is_really_fooable_v<moo>; // 1
}

是的,如果您愿意,您可以将这些特征叠加在一起,例如:
godbolt https://godbolt.org/z/5Yb599de9

struct a {};
struct b {};
struct c {};


void foo(a) { std::cout << "foo1" << std::endl; }

template<class T> inline constexpr bool is_fooable_v = requires(T const& t) { foo(t); };
template<class T> inline constexpr bool is_really_fooable_v = requires(T const& t) { foo(t); };
template<class T> inline constexpr bool is_really_really_fooable_v = requires(T const& t) { foo(t); };


template <class T, class = std::enable_if_t<std::is_same_v<T, b>>>
std::enable_if_t<!is_fooable_v<T>,void> foo(T) { std::cout << "foo2" << std::endl; }

template <class T>
std::enable_if_t<!is_really_fooable_v<T>,void> foo(T) { std::cout << "foo3" << std::endl; }

int main() {
    foo(a{});
    foo(b{});
    foo(c{});
    std::cout << "a: "
              << is_fooable_v<a> << " "
              << is_really_fooable_v<a> << " " 
              << is_really_really_fooable_v<a> << std::endl;
    std::cout << "b: "
              << is_fooable_v<b> << " "
              << is_really_fooable_v<b> << " " 
              << is_really_really_fooable_v<b> << std::endl;
    std::cout << "c: "
              << is_fooable_v<c> << " "
              << is_really_fooable_v<c> << " " 
              << is_really_really_fooable_v<c> << std::endl;
    /* Output:
       foo1
       foo2
       foo3
       a: 1 1 1
       b: 0 1 1
       c: 1 0 1
    */
}

但这会变得非常非常混乱,所以我不会推荐它。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 SFINAE 定义一个要求函数不存在的函数是否可以? 的相关文章

  • 检测到 NuGet 包的版本冲突

    我正在开发 ASP Net core 2 1 Web 应用程序项目 我的解决方案中有 1 个项目和 3 个其他库 它是高级架构 数据访问层 DAL 业务层 BL 公共层 CL 所以我需要添加引用来连接一些库和项目 我已经添加了CL参考我的项
  • Tensorflow 中的自定义资源

    由于某些原因 我需要为 Tensorflow 实现自定义资源 我试图从查找表实现中获得灵感 如果我理解得好的话 我需要实现3个TF操作 创建我的资源 资源的初始化 例如 在查找表的情况下填充哈希表 执行查找 查找 查询步骤 为了促进实施 我
  • 如果.Net Core可以在Windows上运行,为什么不能在.Net Framework中引用.Net Core DLL?

    我明白为什么 Net Framework 可能会在 Net Core IE 中导致问题 因为不存在特定于 Windows 平台的 API 但是为什么不能直接引用 Net Core 作为 Net Framework 中的库呢 如果 Net C
  • 在 C++11 中省略返回类型

    我最近发现自己在 C 11 模式下的 gcc 4 5 中使用了以下宏 define RETURN x gt decltype x return x 并编写这样的函数 template
  • 有什么工具可以说明每种方法运行需要多长时间?

    我的程序的某些部分速度很慢 我想知道是否有我可以使用的工具 例如它可以告诉我可以运行 methodA 花了 100ms 等等 或者类似的有用信息 如果您使用的是 Visual Studio Team System 性能工具 中有一个内置分析
  • 调试内存不足异常

    在修复我制作的小型 ASP NET C Web 应用程序的错误时 我遇到了 OutOfMemoryException 没有关于在哪里查看的提示 因为这是一个编译时错误 如何诊断此异常 我假设这正是内存分析发挥作用的地方 有小费吗 Thank
  • 如何在 VS 中键入时显示方法的完整文档?

    标题非常具有描述性 是否有任何扩展可以让我看到我正在输入的方法的完整文档 我想查看文档 因为我可以在对象浏览器中看到它 其中包含参数的描述和所有内容 而不仅仅是一些 摘要 当然可以选择查看所有覆盖 它可能是智能感知的一部分 或者我不知道它并
  • 组合框项目为空但数据源已满

    将列表绑定到组合框后 其 dataSource Count 为 5 但组合框项目计数为 0 怎么会这样 我习惯了 Web 编程 而且这是在 Windows 窗体中进行的 所以不行combo DataBind 方法存在 这里的问题是 我试图以
  • C# using 语句、SQL 和 SqlConnection

    使用 using 语句 C SQL 可以吗 private static void CreateCommand string queryString string connectionString using SqlConnection c
  • 从匿名类型获取值

    我有一个方法如下 public void MyMethod object obj implement 我这样称呼它 MyMethod new myparam waoww 那么我该如何实施MyMethod 获取 myparam 值 Edit
  • Fluent NHibernate 日期时间 UTC

    我想创建一个流畅的 nhibernate 映射来通过以下方式映射 DateTime 字段 保存时 保存 UTC 值 读取时 调整为本地时区值 实现此映射的最佳方法是什么 就我个人而言 我会将日期存储在 UTC 格式的对象中 然后在读 写时在
  • 同时从多个流中捕获、最佳方法以及如何减少 CPU 使用率

    我目前正在编写一个应用程序 该应用程序将捕获大量 RTSP 流 在我的例子中为 12 个 并将其显示在 QT 小部件上 当我超过大约 6 7 个流时 问题就会出现 CPU 使用率激增并且出现明显的卡顿 我认为它不是 QT 绘制函数的原因是因
  • 如何在 GCC 5 中处理双 ABI?

    我尝试了解如何克服 GCC 5 中引入的双重 ABI 的问题 但是 我没能做到 这是一个重现错误的非常简单的示例 我使用的GCC版本是5 2 如您所见 我的主要函数 在 main cpp 文件中 非常简单 main cpp include
  • 过度使用委托对性能来说是一个坏主意吗? [复制]

    这个问题在这里已经有答案了 考虑以下代码 if IsDebuggingEnabled instance Log GetDetailedDebugInfo GetDetailedDebugInfo 可能是一个昂贵的方法 因此我们只想在调试模式
  • 如何查明CONFIG_FANOTIFY_ACCESS_PERMISSIONS是否启用?

    我想利用fanotify 7 http man7 org linux man pages man7 fanotify 7 html我遇到的问题是在某些内核上CONFIG FANOTIFY ACCESS PERMISSIONS不起作用 虽然C
  • 以编程方式使用自定义元素创建网格

    我正在尝试以编程方式创建一个网格 并将自定义控件作为子项附加到网格中 作为 2x2 矩阵中的第 0 行第 0 列 为了让事情变得更棘手 我使用了 MVVM 设计模式 下面是一些代码可以帮助大家理解这个想法 应用程序 xaml cs base
  • 在基类集合上调用派生方法

    我有一个名为 A 的抽象类 以及实现 A 的其他类 B C D E 我的派生类持有不同类型的值 我还有一个 A 对象的列表 abstract class A class B class A public int val get privat
  • Azure函数版本2.0-应用程序blobTrigger不工作

    我有一个工作功能应用程序 它有一个 blob 输入和一个事件中心输出 在测试版中工作 随着最新的更改 我的功能不再起作用 我尝试根据发行说明更新 host json 文件 但它没有引用 blob 触发器 version 2 0 extens
  • 如何确定母版页中正在显示哪个子页?

    我正在母版页上编写代码 我需要知道正在显示哪个子 内容 页面 我怎样才能以编程方式做到这一点 我用这个 string pageName this ContentPlaceHolder1 Page GetType FullName 它以 AS
  • WPF/数据集:如何通过 XAML 将相关表中的数据绑定到数据网格列中?

    我正在使用 WPF DataSet 连接到 SQL Server Express XAML 和 C Visual Studio 2013 Express 我从名为 BankNoteBook 的现有 SQL Server Express 数据

随机推荐