首页 > 解决方案 > 检查(全局)函数是否存在,但不允许隐式转换

问题描述

考虑这个简单的检查是否定义了(全局)函数:

template <typename T>
concept has_f = requires ( const T& t ) { Function( t ); };
// later use in MyClass<T>:
if constexpr ( has_f<T> ) Function( value );

不幸的是,这允许隐式转换。这显然是一个很大的混乱风险。

问题:如何检查 Function( const T& t ) 是否“明确”存在?

就像是

if constexpr ( std::is_same_v<decltype( Function( t ) ), void> )

应该没有隐式转换,但我无法让它工作。

注意:概念方法的重点是摆脱旧的“检测模式”并简化。

标签: c++functiontemplatesimplicit-conversionc++20

解决方案


在解释如何做到这一点之前,我将解释为什么你不应该这样做。

您提到了“旧的'检测模式'”,但没有添加任何关于您所指内容的细节。有很多 C++ 用户有时使用的惯用语可以做一些事情,比如检测一个函数是否接受一个特定的参数。您认为其中哪些算作“检测模式”尚不清楚。

然而,绝大多数这些习惯用法的存在是为了一个特定的、单一的目的:查看具有给定参数集的特定函数调用是否是有效的、合法的 C++ 代码。他们并不真正关心函数是否完全需要T;专门测试T只是其中一些习语如何产生重要信息。即是否可以将 a 传递T给所述函数。

寻找特定的函数签名几乎总是达到目的的手段,而不是最终目标。

概念,特别是需要表达的,是目的本身。它允许您直接提出问题。因为真的,你不在乎是否Function有一个带T;的参数。你关心是否Function(t)是合法的代码。具体如何发生是一个实现细节。

我能想到有人可能想要将模板限制在精确签名(而不是参数匹配)上的唯一原因是为了打败隐式转换。但是你真的不应该尝试破坏这样的基本语言特性。如果有人编写的类型可以隐式转换为另一种类型,则他们有权享受这种转换带来的好处,正如语言所定义的那样。也就是说,能够以多种方式使用它,就好像它是其他类型一样。

也就是说,如果Function(t)受约束的模板代码实际上要执行的操作,那么该模板的用户有权提供使该编译器处于 C++ 语言限制范围内的代码。不在您对该语言中哪些功能好坏的个人想法的范围内。

概念不像基类,你决定每个方法的确切签名,用户必须严格遵守。概念是约束模板定义的模式。概念约束中的表达式是您希望在模板中使用的表达式。如果您计划在受该概念约束的模板中使用表达式,则只能将表达式放入概念中。

您不使用函数签名;你调用函数。因此,您限制了可以使用哪些参数调用哪些函数的概念。您是在说“您必须让我这样做”,而不是“提供此签名”。


话虽如此......你想要的通常是不可能的;)

您可以使用多种机制来实现它,但它们都不能在所有情况下都完全符合您的要求。

函数的名称解析为由所有可以调用的函数组成的重载集。当且仅当该签名是重载集中的函数之一时,此名称才能转换为指向特定函数签名的指针。所以理论上,你可以这样做:

template <typename T>
concept has_f = requires () { static_cast<void (*)(T const&)>(&Function); };

但是,由于名称Function不依赖于(TC++ 而言),它必须在模板的两阶段名称查找的第一遍中解析。Function这意味着您打算关心的任何和所有重载都必须在定义之前 声明,而不仅仅是用适当的.has_fT

我认为这足以声明这是一种非功能性的解决方案。即使它有效,它也只会在 3 种情况下“有效”:

  1. Function已知/必须是实际函数,而不是具有operator()重载的全局对象。因此,如果提供者T想要提供一个全局仿函数而不是常规函数(出于多种原因),则此方法将不起作用,即使它Function(t)是 100% 完全有效、合法的,并且不会执行对某些人而言的那些可怕的隐式转换理由必须停止。

  2. 该表达式Function(t)不应使用 ADL 来查找Function要调用的实际值。

  3. Function不是模板函数。

这些可能性中没有一种与隐式转换有关。如果你要调用Function(t),那么 ADL 100% 可以找到它,模板参数推导来实例化它,或者用户用一些全局 lambda 来实现它。

您的第二个最佳选择是依靠重载解决方案的工作原理。C++ 仅允许在运算符重载中进行单个用户定义的转换。因此,您可以创建一种类型,该类型将在函数调用表达式中使用该用户定义的转换来代替T. 这种转变应该是对T自身的转变。

你会像这样使用它:

template<typename T>
class udc_killer
{
public:
  //Will never be called.
  operator T const&();
};

template <typename T>
concept has_f = requires () { Function(udc_killer<T>{}); };

这当然仍然保留标准转换,因此您无法区分采用floatif Tis的函数int或来自基类的派生类。您也无法检测Function在第一个参数之后是否有任何默认参数。

总的来说,你仍然没有检测到签名,只是调用能力。因为这就是你开始时应该关心的一切。


推荐阅读