我有以下模板类作为代理。它有一个名为call
的方法,它应该用于调用包装对象的方法。这有问题。类型演绎失败,我无法理解为什么。
Hudsucker::f
需要std::string
,然后无论我是否传递std::string
或const
引用,编译器都可以调用正确的方法。
但是如果Hudsucker::g
带有const
引用std::string
类型,那么在GCC和Clang两种情况下,类型扣除都会失败。
第一行的GCC错误:
main.cpp:36:28: error: no matching function for call to ‘Proxy<Hudsucker>::call(void (Hudsucker::*)(const string&), const string&)’
main.cpp:36:28: note: candidate is:
main.cpp:10:10: note: template<class A> void Proxy::call(void (T::*)(A), A) [with A = A; T = Hudsucker]
main.cpp:10:10: note: template argument deduction/substitution failed:
main.cpp:36:28: note: deduced conflicting types for parameter ‘A’ (‘const std::basic_string<char>&’ and ‘std::basic_string<char>’)
特别是这一点很奇怪:no matching function for call to Proxy<Hudsucker>::call(void (Hudsucker::*)(const string&), const string&)
。这正是我希望看到工作的签名。
第一行的Clang错误:
main.cpp:36:7: error: no matching member function for call to 'call'
p.call(&Hudsucker::g, s); // <- Compile error
~~^~~~
main.cpp:10:10: note: candidate template ignored: deduced conflicting types for parameter 'A' ('const std::basic_string<char> &' vs. 'std::basic_string<char>')
void call(void (T::*f)(A), A a)
代码:
#include <string>
#include <iostream>
template <typename T> class Proxy
{
public:
Proxy(T &o): o_(o) {}
template <typename A>
void call(void (T::*f)(A), A a)
{
(o_.*f)(a);
}
private:
T &o_;
};
class Hudsucker
{
public:
void f(std::string s) {}
void g(std::string const &s) {}
};
int main()
{
Hudsucker h;
Proxy<Hudsucker> p(h);
std::string const s = "For kids, you know.";
std::string const &r = s;
p.call(&Hudsucker::f, s);
p.call(&Hudsucker::f, r);
p.call(&Hudsucker::g, s); // <- Compile error
p.call(&Hudsucker::g, r); // <- Compile error
return 0;
}
你能解释为什么类型演绎以这种方式失败了吗?有没有办法让这个用const
引用进行编译?
答案 0 :(得分:11)
编译器无法推断出类型A
,因为它具有对比的信息。从成员函数的类型,它将A
推导为std::string const&
,而从第二个参数的类型,它将推导为std::string
。
将您的函数模板更改为允许成员函数的参数和实际提供的参数的不同类型的函数模板,然后SFINAE约束后者可转换为前者:
template <typename A, typename B,
typename std::enable_if<std::is_convertible<B, A>::value>::type* = nullptr>
void call(void (T::*f)(A), B a)
{
(o_.*f)(a);
}
如果您想知道为什么要从这个函数调用:
std::string const s = "For kids, you know.";
// ...
p.call(&Hudsucker::g, s);
编译器将推导出std::string
,这是因为C ++ 11标准的第14.8.2.1/2段:
如果
P
不是参考类型:- 如果
A
是数组类型,则数组到指针标准转换(4.2)生成的指针类型是 用于代替A
进行类型扣除;否则,- 如果
A
是函数类型,则函数到指针标准转换生成的指针类型(4.3) 用于代替A
进行类型扣除;否则,- 如果A是cv限定类型,则
A
类型的顶级cv限定符将被忽略以进行类型扣除。
在引用的段落中,P
是您的A
(来自您的功能模板),A
是std::string const
。这意味着类型扣除会忽略const
中的std::string const
。要更好地了解这一点,请考虑以下更简单的示例:
#include <type_traits>
template<typename T>
void foo(T t)
{
// Does NOT fire!
static_assert(std::is_same<T, int>::value, "!");
}
int main()
{
int const x = 42;
foo(x);
}
考虑第二个函数调用:
std::string const &r = s;
// ...
p.call(&Hudsucker::g, r);
原因是 id-expression r
的类型为std::string const
。由于第5/5段,该引用被删除:
如果表达式最初具有类型“
T
”的引用(8.3.2,8.5.3),则类型将在T
之前调整为#include <utility> // For std::forward<>() template <typename A, typename B, typename std::enable_if<std::is_convertible<B, A>::value>::type* = nullptr> void call(void (T::*f)(A), B&& a) { (o_.*f)(std::forward<B>(a)); }
任何进一步的分析。表达式指定由引用表示的对象或函数,以及 表达式是左值或左值,具体取决于表达式。
现在我们又回到了与第一个函数调用相同的情况。
正如Mike Vine在评论中指出的那样,在函数调用期间输入第一个(成员函数)参数时,你可能希望完美地转发你的第二个参数:
template <typename A, typename B>
typename enable_if<is_convertible<B, A>::value>::type
// ^^^^^^^^^ ^^^^^^^^^^^^^^
// But how about these traits?
call(void (T::*f)(A), B a)
{
(o_.*f)(a);
}
如果您买不起C ++ 11,则不允许您使用模板参数的默认参数。在这种情况下,您可以在返回类型上使用SFINAE约束:
std::enable_if
请注意,std::is_convertible
和enable_if
不是C ++ 03标准库的一部分。幸运的是,Boost有自己的版本is_convertible
和#include <boost/utility/enable_if.hpp>
#include <boost/type_traits/is_convertible.hpp>
template <typename T> class Proxy
{
public:
Proxy(T &o): o_(o) {}
template <typename A, typename B>
typename boost::enable_if<boost::is_convertible<B, A>>::type
call(void (T::*f)(A), B a)
{
(o_.*f)(a);
}
private:
T &o_;
};
,所以:
boost::enable_if
请注意,value
接受第一个模板参数 type ,它定义了std::enable_if
布尔成员,而std::enable_if
接受布尔值。 Boost中的boost::enable_if_c
相当于{{1}}。
答案 1 :(得分:4)
对我来说,一个更简单的解决方案是将两个参数中的一个排除在试图推导出A之后,第二个是更好的候选者:
template <typename A>
void call(void (T::*f)(A), typename std::identity<A>::type a)
{
(o_.*f)(a);
}
如果您的类型特征中没有std::identity
,请使用以下字符:
template <typename T>
struct identity { typedef T type; };
这就是为什么这样做的原因:编译器不能从第二个参数中推导出A,因为它只是一个嵌套类型的模板参数。基本上,它不能将任何传入类型与something_that_contains_A :: type进行模式匹配 - 由于模板特化,它无法对左侧定义中的参数进行反向工程。最终结果是第二个参数是“未受限制的上下文”。编译器不会尝试从那里推断出A.
这使得第一个参数成为唯一可以推导出A的地方。只有一个A的演绎结果,它不含糊,演绎成功。然后编译器继续将演绎结果替换为使用A的每个地方,包括第二个参数。
答案 2 :(得分:1)
在main中调用模板函数时,只需将模板参数传递给模板函数。
int main()
{
Hudsucker h;
Proxy<Hudsucker> p(h);
std::string const s = "For kids, you know.";
std::string const &r = s;
p.call(&Hudsucker::f, s);
p.call(&Hudsucker::f, r);
//just add template argument to template function call !!!
p.call< const std::string & > (&Hudsucker::g, s); // <- NO Compile error !!!!
p.call< const std::string & > (&Hudsucker::g, r); // <- NO Compile error !!!**
return 0;
}