繁体   English   中英

使用 std::function 重载解析

[英]Overload resolution with std::function

考虑这个代码示例:

#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!\n"; });
}

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

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

在 C++11...

我们来看看std::function的构造函数模板的规范(它接受任何 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要求: F应为CopyConstructible f应为参数类型ArgTypes和返回类型R Callable (20.10.11.2)。 的拷贝构造函数和析构函数A不抛出异常。

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

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

9 否则, *this以使用std::move(f)初始化的f的副本为目标。 [这里省略了一个注释]

10抛出:当不得抛出异常f是一个函数指针或一个reference_wrapper<T>对于一些T 否则,可能会抛出bad_allocF的复制或移动构造函数抛出的任何异常。

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

[res.on.required]/1

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

因此,AFAIK,即使重载决议的结果也是未定义的。 因此,g++/libstdc++ 的两个版本在这方面都符合。


在 C++14 中,这已更改,请参阅LWG 2132 现在,需要std::function的转换构造函数模板来拒绝 SFINAE 不兼容的可调用对象(下一章将详细介绍 SFINAE):

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

7要求: F应为CopyConstructible

8备注:除非f对于参数类型ArgTypes...和返回类型R是 Callable (20.9.11.2),否则这些构造函数不应参与重载决议。

[...]

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

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

和一个调用表达式,例如

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

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

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

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

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

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


SFINAE 如何在此提供帮助?

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

#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\n";  }

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

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

类似地,可以通过在转换构造函数上使用 SFINAE 使转换不可行:

#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)\n";  }
};

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

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

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

这是完全有效的。 由于 c++11 lambda 表达式(和您的std::function包装器)创建函数对象。 函数对象的强大之处在于,即使它们是泛型的,它们仍然是一流的对象。 与普通的函数模板不同,它们可以传递给函数和从函数返回。

您可以使用继承和 using 声明显式创建运算符重载集。 Mathias Gaunard的以下用法演示了“重载的 lambda 表达式”。

template <class F1, class F2>
struct overload_set : F1, F2
{
    overload_set(F1 x1, F2 x2) : F1(x1), F2(x2) {}
    using F1::operator();
    using F2::operator();
};

template <class F1, class F2>
overload_set<F1,F2> overload(F1 x1, F2 x2)
{
    return overload_set<F1,F2>(x1,x2);
}

auto f = overload(
    [](){return 1;}, 
    [](int x){return x+1;}
);

int x = f();
int y = f(2);

来源

编辑:如果在提供的示例中替换,也许会变得更清楚

F1 -> std::function<void()> 
F2 -> std::function<void(int)>

查看它在 gcc4.7 中编译

提供模板化解决方案只是为了证明概念可以扩展到通用代码并且消除歧义是可能的。

在您的情况下,当使用像 gcc 4.7 这样的旧编译器时,您可以通过显式转换来提供帮助,并且gcc 会解决问题,正如您在此实时示例中所看到的

以防万一你想知道,如果你反过来转换它就行不通了(尝试将带 int 的 lambda 转换为不带参数的 std::function 等等)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM