3

I have the following template class which acts as a proxy. It has a method named call which is supposed to be used to call methods on the wrapped object. There's a problem with it. The type deduction fails and I cannot understand why.

Hudsucker::f takes an std::string and then no matter if I pass an std::string or a const reference to it the compiler is able to call the right method.

But in case of Hudsucker::g with takes a const reference to std::string type deduction fails in both cases with both GCC and Clang.

GCC error for the first line:

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>’)

Especially this bit is strange: no matching function for call to Proxy<Hudsucker>::call(void (Hudsucker::*)(const string&), const string&). That is exactly the signature I would expect to see work.

Clang error for the first line:

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)

Code:

#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;
}

Could you explain why the type deduction fails in that way? Is there a way to get this to compile with const references?

4

3 回答 3

12

编译器无法推断 type A,因为它具有对比信息。从成员函数的类型推导出Astd::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)产生的指针类型代替Afor类型推导;否则,

— ifA是函数类型,使用函数到指针标准转换(4.3)产生的指针类型代替Afor类型推导;否则,

—如果 A 是 cv 限定类型,则类型推导的顶级 cv 限定符将A被忽略。

在引用的段落中,Pis your A(from your function template) and Ais std::string const. 这意味着constinstd::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 段,引用被删除:

如果表达式最初的类型为“reference to T”(8.3.2、8.5.3),则T在进一步分析之前将类型调整为。表达式指定引用表示的对象或函数,表达式是左值或 x 值,具体取决于表达式。

现在我们回到与第一个函数调用相同的情况。


正如 Mike Vine 在评论中指出的那样,在函数调用期间将第二个参数作为输入提供给第一个(成员函数)参数时,您可能希望完美地转发第二个参数:

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

如果你买不起 C++11,那么你将不能使用模板参数的默认参数。在这种情况下,您可以在返回类型上使用 SFINAE 约束:

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);
}

请注意,它std::enable_if不是std::is_convertibleC++03 标准库的一部分。幸运的是,Boost 有自己的enable_ifand版本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作为其第一个模板参数,而接受一个布尔值。Boost 中的等价物是.valuestd::enable_ifstd::enable_ifboost::enable_if_c

于 2013-05-28T14:33:13.210 回答
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 的每个地方,包括第二个参数。

于 2013-05-28T15:23:36.203 回答
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;

}

于 2014-10-08T18:22:47.393 回答