使用 std::function 重载解析

2021-12-23 00:00:00 lambda overloading c++ c++11

考虑这个代码示例:

#include #include <功能>typedef std::functionfunc1_t;typedef std::functionfunc2_t;结构体{X (func1_t f){ }X (func2_t f){ }};内部主(){X x([](){ std::cout << "Hello, world!
"; });}

我确信它不应该编译,因为编译器不应该能够选择两个构造函数之一.g++-4.7.3 显示了这种预期行为:它表示重载构造函数的调用不明确.但是g++-4.8.2编译成功.

这段代码在 C++11 中是正确的还是这个版本的 g++ 的错误/特性?

解决方案

In C++11...

我们来看看std::function的构造函数模板的规范(它接受任何Callable):[func.wrap.func.con]/7-10

<块引用>

template函数(F f);模板<F类,A类>函数(allocator_arg_t,const A& a,F f);

7 要求: F 应该是 CopyConstructible.对于参数类型 ArgTypes 和返回类型,f 应为 Callable (20.10.11.2)R.A 的拷贝构造函数和析构函数不得抛出例外.

8 后置条件: !*this 如果以下任何一项成立:

  • f 是一个 NULL 函数指针.
  • f 是指向成员的 NULL 指针.
  • F 是函数类模板的实例,!f

9 否则,*thisstd::move(f) 初始化的f 副本为目标.[这里省略了一个注释]

10 抛出:当 f 是函数指针或 reference_wrapper 时不应抛出异常,用于某些 T.否则,可能会抛出bad_allocF 的复制或移动构造函数抛出的任何异常.

现在,从 [](){} 构建或尝试构建(用于重载解析)std::function(即带有签名void(void))违反了std::function的构造函数的要求.

[res.on.required]/1

<块引用>

违反函数的 Requires: 段落中指定的先决条件会导致未定义的行为,除非函数的 Throws: 段落指定在违反先决条件时抛出异常.>

所以,AFAIK,即使是重载决议的结果也是未定义的.因此,g++/libstdc++ 的两个版本都符合这一点.

<小时>

在 C++14 中,这已更改,请参阅 LWG 2132.现在,std::function 的转换构造函数模板需要 SFINAE-reject 不兼容的 Callables(下一章将详细介绍 SFINAE):

<块引用>

template函数(F f);模板<F类,A类>函数(allocator_arg_t,const A& a,F f);

7 要求: F 应该是 CopyConstructible.

8 备注: 这些构造函数不应参与重载解析,除非 f 对于参数类型是可调用的 (20.9.11.2)ArgTypes... 和返回类型 R.

[...]

不应参与重载决议"对应于通过 SFINAE 的拒绝.最终效果是,如果您有一组重载函数 foo,

void foo(std::function);void foo(std::function);

和一个调用表达式,例如

foo([](std::string){})//(C)

然后 foo 的第二个重载被明确选择:因为 std::functionF 定义为它的外部接口,F 定义了哪些参数类型被传递到 std::function.然后,必须使用这些参数(参数类型)调用包装的函数对象.如果将 double 传递给 std::function,则它不能传递给采用 std::string 的函数,因为没有转换 double -> std::string.对于 foo 的第一次重载,参数 [](std::string){} 因此不被视为可调用用于 std::function.构造函数模板已停用,因此没有从 [](std::string){}std::function 的可行转换.第一个重载从用于解析调用 (C) 的重载集中移除,只留下第二个重载.

请注意,由于 LWG 2420,上述措辞略有变化:有一个例外,如果 std::function 的返回类型 Rvoid,则上述构造函数模板中的 Callable 的任何返回类型都将被接受(并丢弃).例如, []() ->void {}[]() ->bool {} 对于 std::function 是可调用的.因此,以下情况会产生歧义:

void foo(std::function);void foo(std::function);foo([]() -> bool {});//模糊的

重载解析规则不会尝试在不同的用户定义转换之间进行排名,因此 foo 的两个重载都是可行的(首先)并且两者都不是更好.

<小时>

SFINAE 在这方面有什么帮助?

请注意,当 SFINAE 检查失败时,程序不是格式错误的,但该函数不适用于重载解析.例如:

#include #include 模板自动 foo(T) ->类型名称 std::enable_if自动 foo(T) ->类型名称 std::enable_if<不是 std::is_integral<T>::value >::type{ std::cout <<"foo 2
";}int main(){富(42);富(42.);}

同样,可以通过在转换构造函数上使用 SFINAE 使转换不可行:

#include #include 结构体{模板<类T,类=类型名称 std::enable_if

Consider this example of code:

#include <iostream>
#include <functional>

typedef std::function<void()> func1_t;
typedef std::function<void(int)> func2_t;

struct X
{
   X (func1_t f)
   { }

   X (func2_t f)
   { }
};

int main ( )
{
   X x([](){ std::cout << "Hello, world!
"; });
}

I was sure that it shouldn't compile, because the compiler shouldn't be able to choose one of the two constructors. g++-4.7.3 shows this expected behavior: it says that call of overloaded constructor is ambiguous. However, g++-4.8.2 successfully compiles it.

Is this code correct in C++11 or it is a bug/feature of this version of g++?

解决方案

In C++11...

Let's take a look at the specification of the constructor template of std::function (which takes any Callable): [func.wrap.func.con]/7-10

template<class F> function(F f);
template <class F, class A> function(allocator_arg_t, const A& a, F f);

7 Requires: F shall be CopyConstructible. f shall be Callable (20.10.11.2) for argument types ArgTypes and return type R. The copy constructor and destructor of A shall not throw exceptions.

8 Postconditions: !*this if any of the following hold:

  • f is a NULL function pointer.
  • f is a NULL pointer to member.
  • F is an instance of the function class template, and !f

9 Otherwise, *this targets a copy of f initialized with std::move(f). [left out a note here]

10 Throws: shall not throw exceptions when f is a function pointer or a reference_wrapper<T> for some T. Otherwise, may throw bad_alloc or any exception thrown by F’s copy or move constructor.

Now, constructing, or attempting to construct (for overload resolution) a std::function<void(int)> from a [](){} (i.e. with signature void(void)) violates the requirements of std::function<void(int)>'s constructor.

[res.on.required]/1

Violation of the preconditions specified in a function’s Requires: paragraph results in undefined behavior unless the function’s Throws: paragraph specifies throwing an exception when the precondition is violated.

So, AFAIK, even the result of the overload resolution is undefined. Therefore, both versions of g++/libstdc++ are complying in this aspect.


In C++14, this has been changed, see LWG 2132. Now, the converting constructor template of std::function is required to SFINAE-reject incompatible Callables (more about SFINAE in the next chapter):

template<class F> function(F f);
template <class F, class A> function(allocator_arg_t, const A& a, F f);

7 Requires: F shall be CopyConstructible.

8 Remarks: These constructors shall not participate in overload resolution unless f is Callable (20.9.11.2) for argument types ArgTypes... and return type R.

[...]

The "shall not participate in overload resolution" corresponds to rejection via SFINAE. The net effect is that if you have an overload set of functions foo,

void foo(std::function<void(double)>);
void foo(std::function<void(char const*)>);

and a call-expression such as

foo([](std::string){}) // (C)

then the second overload of foo is chosen unambiguously: Since std::function<F> defines F as its interface to the outside, the F defines which argument types are passed into std::function. Then, the wrapped function object has to be called with those arguments (argument types). If a double is passed into std::function, it cannot be passed on to a function taking a std::string, because there's no conversion double -> std::string. For the first overload of foo, the argument [](std::string){} is therefore not considered Callable for std::function<void(double)>. The constructor template is deactivated, hence there's no viable conversion from [](std::string){} to std::function<void(double)>. This first overload is removed from the overload set for resolving the call (C), leaving only the second overload.

Note that there's been a slight change to the wording above, due to LWG 2420: There's an exception that if the return type R of a std::function<R(ArgTypes...)> is void, then any return type is accepted (and discarded) for the Callable in the constructor template mentioned above. For example, both []() -> void {} and []() -> bool {} are Callable for std::function<void()>. The following situation therefore produces an ambiguity:

void foo(std::function<void()>);
void foo(std::function<bool()>);

foo([]() -> bool {}); // ambiguous

The overload resolution rules don't try to rank among different user-defined conversions, and hence both overloads of foo are viable (first of all) and neither is better.


How can SFINAE help here?

Note when a SFINAE-check fails, the program isn't ill-formed, but the function isn't viable for overload resolution. For example:

#include <type_traits>
#include <iostream>

template<class T>
auto foo(T) -> typename std::enable_if< std::is_integral<T>::value >::type
{  std::cout << "foo 1
";  }

template<class T>
auto foo(T) -> typename std::enable_if< not std::is_integral<T>::value >::type
{  std::cout << "foo 2
";  }

int main()
{
    foo(42);
    foo(42.);
}

Similarly, a conversion can be made non-viable by using SFINAE on the converting constructor:

#include <type_traits>
#include <iostream>

struct foo
{
    template<class T, class =
             typename std::enable_if< std::is_integral<T>::value >::type >
    foo(T)
    {  std::cout << "foo(T)
";  }
};

struct bar
{
    template<class T, class =
             typename std::enable_if< not std::is_integral<T>::value >::type >
    bar(T)
    {  std::cout << "bar(T)
";  }
};

struct kitty
{
    kitty(foo) {}
    kitty(bar) {}
};

int main()
{
    kitty cat(42);
    kitty tac(42.);
}

相关文章