为什么 std::function 不参与重载决议?
Why doesn't std::function participate in overload resolution?
我知道以下代码无法编译。
void baz(int i) { }
void baz() { }
class Bar
{
std::function<void()> bazFn;
public:
Bar(std::function<void()> fun = baz) : bazFn(fun){}
};
int main(int argc, char **argv)
{
Bar b;
return 0;
}
因为据说 std::function
不考虑重载决议,正如我在 中读到的那样。
我不完全理解强制采用这种解决方案的技术限制。
我在 cppreference 上读到了 phases of translation and templates,但我想不出任何我找不到反例的推理。向半外行(对 C++ 还是新手)解释一下,是什么以及在哪个翻译阶段导致上述编译失败?
这与 "phases of translation" 没有任何关系。这纯粹是关于 std::function
.
的构造函数
看,std::function<R(Args)>
不要求给定的函数完全 属于 R(Args)
类型。特别是,它不需要给它一个函数指针。它可以采用任何可调用类型(成员函数指针,某些具有 operator()
重载的对象)只要它是可调用的 就像 它采用 Args
参数一样并且 return 可以 转换为 R
(或者如果 R
是 void
,它可以 return 任何东西)。
为此,std::function
的适当构造函数必须是 模板 :template<typename F> function(F f);
。即可以取任意函数类型(受上述限制)。
表达式baz
表示一个重载集。如果您使用该表达式来调用重载集,那很好。如果将该表达式用作采用特定函数指针的函数的参数,C++ 可以将重载集缩减为单个调用,从而使其正常。
但是,一旦函数是模板,并且您正在使用模板参数推导来确定该参数是什么,C++ 就不再能够确定重载集中的正确重载是什么。所以必须直接指定。
这里的问题是没有告诉编译器如何执行指针衰减函数。如果你有
void baz(int i) { }
void baz() { }
class Bar
{
void (*bazFn)();
public:
Bar(void(*fun)() = baz) : bazFn(fun){}
};
int main(int argc, char **argv)
{
Bar b;
return 0;
}
然后代码就可以工作了,因为现在编译器知道你想要哪个函数,因为有一个你要分配给的具体类型。
当您使用 std::function
时,您调用它的函数对象构造函数,其形式为
template< class F >
function( F f );
并且由于是模板,所以需要推导传递的对象的类型。因为 baz
是一个重载函数,所以没有可以推导的单一类型,因此模板推导失败并且你得到一个错误。你必须使用
Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}
获取强制单类型并允许扣除。
仅当 (a) 调用 function/operator 的名称或 (b) 将其转换为具有显式签名的指针(指向函数或成员函数)时,才会发生重载解析。
这里都没有发生。
std::function
接受任何与其签名兼容 的对象。它不专门采用函数指针。 (lambda 不是 std 函数,std 函数也不是 lambda)
现在在我的自制函数变体中,对于签名 R(Args...)
我也接受 R(*)(Args...)
参数(完全匹配)正是出于这个原因。但这意味着它将 "exact match" 个签名提升到 "compatible" 个签名之上。
核心问题是重载集不是C++对象。您可以命名重载集,但不能将其传递给 "natively".
现在,您可以像这样创建函数的伪重载集:
#define RETURNS(...) \
noexcept(noexcept(__VA_ARGS__)) \
-> decltype(__VA_ARGS__) \
{ return __VA_ARGS__; }
#define OVERLOADS_OF(...) \
[](auto&&...args) \
RETURNS( __VA_ARGS__(decltype(args)(args)...) )
这将创建一个 C++ 对象,该对象可以对函数名称进行重载解析。
扩展宏,我们得到:
[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }
写起来很烦人。一个更简单但用处稍差的版本在这里:
[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }
我们有一个 lambda 接受任意数量的参数,然后将它们完美转发给 baz
。
然后:
class Bar {
std::function<void()> bazFn;
public:
Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};
有效。我们将重载解析推迟到我们存储在 fun
中的 lambda 中,而不是直接传递 fun
一个重载集(它无法解析)。
至少有一项提议在 C++ 语言中定义一个将函数名转换为重载集对象的操作。在标准中出现这样的标准提案之前,OVERLOADS_OF
宏很有用。
您可以更进一步,支持转换为兼容函数指针。
struct baz_overloads {
template<class...Ts>
auto operator()(Ts&&...ts)const
RETURNS( baz(std::forward<Ts>(ts)...) );
template<class R, class...Args>
using fptr = R(*)(Args...);
//TODO: SFINAE-friendly support
template<class R, class...Ts>
operator fptr<R,Ts...>() const {
return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
}
};
但这开始变得迟钝了。
#define OVERLOADS_T(...) \
struct { \
template<class...Ts> \
auto operator()(Ts&&...ts)const \
RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
template<class R, class...Args> \
using fptr = R(*)(Args...); \
\
template<class R, class...Ts> \
operator fptr<R,Ts...>() const { \
return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
} \
}
此时编译器正在决定将哪个重载传递给 std::function
构造函数,它所知道的是 std::function
构造函数被模板化为采用任何类型。它没有能力尝试两个重载并发现第一个不编译但第二个可以编译。
解决这个问题的方法是用 static_cast
:
显式告诉编译器你想要哪个重载
Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
我知道以下代码无法编译。
void baz(int i) { }
void baz() { }
class Bar
{
std::function<void()> bazFn;
public:
Bar(std::function<void()> fun = baz) : bazFn(fun){}
};
int main(int argc, char **argv)
{
Bar b;
return 0;
}
因为据说 std::function
不考虑重载决议,正如我在
我不完全理解强制采用这种解决方案的技术限制。
我在 cppreference 上读到了 phases of translation and templates,但我想不出任何我找不到反例的推理。向半外行(对 C++ 还是新手)解释一下,是什么以及在哪个翻译阶段导致上述编译失败?
这与 "phases of translation" 没有任何关系。这纯粹是关于 std::function
.
看,std::function<R(Args)>
不要求给定的函数完全 属于 R(Args)
类型。特别是,它不需要给它一个函数指针。它可以采用任何可调用类型(成员函数指针,某些具有 operator()
重载的对象)只要它是可调用的 就像 它采用 Args
参数一样并且 return 可以 转换为 R
(或者如果 R
是 void
,它可以 return 任何东西)。
为此,std::function
的适当构造函数必须是 模板 :template<typename F> function(F f);
。即可以取任意函数类型(受上述限制)。
表达式baz
表示一个重载集。如果您使用该表达式来调用重载集,那很好。如果将该表达式用作采用特定函数指针的函数的参数,C++ 可以将重载集缩减为单个调用,从而使其正常。
但是,一旦函数是模板,并且您正在使用模板参数推导来确定该参数是什么,C++ 就不再能够确定重载集中的正确重载是什么。所以必须直接指定。
这里的问题是没有告诉编译器如何执行指针衰减函数。如果你有
void baz(int i) { }
void baz() { }
class Bar
{
void (*bazFn)();
public:
Bar(void(*fun)() = baz) : bazFn(fun){}
};
int main(int argc, char **argv)
{
Bar b;
return 0;
}
然后代码就可以工作了,因为现在编译器知道你想要哪个函数,因为有一个你要分配给的具体类型。
当您使用 std::function
时,您调用它的函数对象构造函数,其形式为
template< class F >
function( F f );
并且由于是模板,所以需要推导传递的对象的类型。因为 baz
是一个重载函数,所以没有可以推导的单一类型,因此模板推导失败并且你得到一个错误。你必须使用
Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}
获取强制单类型并允许扣除。
仅当 (a) 调用 function/operator 的名称或 (b) 将其转换为具有显式签名的指针(指向函数或成员函数)时,才会发生重载解析。
这里都没有发生。
std::function
接受任何与其签名兼容 的对象。它不专门采用函数指针。 (lambda 不是 std 函数,std 函数也不是 lambda)
现在在我的自制函数变体中,对于签名 R(Args...)
我也接受 R(*)(Args...)
参数(完全匹配)正是出于这个原因。但这意味着它将 "exact match" 个签名提升到 "compatible" 个签名之上。
核心问题是重载集不是C++对象。您可以命名重载集,但不能将其传递给 "natively".
现在,您可以像这样创建函数的伪重载集:
#define RETURNS(...) \
noexcept(noexcept(__VA_ARGS__)) \
-> decltype(__VA_ARGS__) \
{ return __VA_ARGS__; }
#define OVERLOADS_OF(...) \
[](auto&&...args) \
RETURNS( __VA_ARGS__(decltype(args)(args)...) )
这将创建一个 C++ 对象,该对象可以对函数名称进行重载解析。
扩展宏,我们得到:
[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }
写起来很烦人。一个更简单但用处稍差的版本在这里:
[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }
我们有一个 lambda 接受任意数量的参数,然后将它们完美转发给 baz
。
然后:
class Bar {
std::function<void()> bazFn;
public:
Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};
有效。我们将重载解析推迟到我们存储在 fun
中的 lambda 中,而不是直接传递 fun
一个重载集(它无法解析)。
至少有一项提议在 C++ 语言中定义一个将函数名转换为重载集对象的操作。在标准中出现这样的标准提案之前,OVERLOADS_OF
宏很有用。
您可以更进一步,支持转换为兼容函数指针。
struct baz_overloads {
template<class...Ts>
auto operator()(Ts&&...ts)const
RETURNS( baz(std::forward<Ts>(ts)...) );
template<class R, class...Args>
using fptr = R(*)(Args...);
//TODO: SFINAE-friendly support
template<class R, class...Ts>
operator fptr<R,Ts...>() const {
return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
}
};
但这开始变得迟钝了。
#define OVERLOADS_T(...) \
struct { \
template<class...Ts> \
auto operator()(Ts&&...ts)const \
RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
template<class R, class...Args> \
using fptr = R(*)(Args...); \
\
template<class R, class...Ts> \
operator fptr<R,Ts...>() const { \
return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
} \
}
此时编译器正在决定将哪个重载传递给 std::function
构造函数,它所知道的是 std::function
构造函数被模板化为采用任何类型。它没有能力尝试两个重载并发现第一个不编译但第二个可以编译。
解决这个问题的方法是用 static_cast
:
Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}