std::apply内使用std::make_shared编译失败的原因排查
嘿,这个问题其实是函数模板实例化后的签名不匹配导致的,核心是std::make_shared的完美转发机制和std::apply传递参数的方式撞车了。咱们一步步拆解清楚:
1. 你的自定义my_own_make_shared为啥能跑通?
先看你写的这个辅助函数:
template<typename T, typename... Args> std::shared_ptr<T> my_own_make_shared(Args... args) { return std::make_shared<T>(args...); }
当你在A<Test, int, int>::get()里调用std::apply(my_own_make_shared<Test, int, int>, args_)时,这个函数会被实例化成:
std::shared_ptr<Test> my_own_make_shared(int, int);
参数是值类型,而std::apply从你的tuple(args_是左值引用)里取出的是int&(左值),左值可以隐式转换为值类型(会做拷贝),所以编译器能找到匹配的函数,自然没问题。
2. std::make_shared<T, Args...>为啥会炸?
std::make_shared的标准签名是这样的:
template<class T, class... Args> std::shared_ptr<T> make_shared(Args&&... args);
这里的Args&&是完美转发用的右值引用,但当你显式指定模板参数std::make_shared<Test, int, int>时,Args被固定成了int, int,所以这个实例化后的函数签名就变成了:
std::shared_ptr<Test> make_shared(int&&, int&&);
参数是右值引用!而std::apply处理你的args_(左值tuple)时,会把里面的元素以左值引用的方式传给目标函数。C++里右值引用是不能直接绑定左值的,这就导致了编译错误——编译器找不到能匹配的函数调用,因为int&转不成int&&。
你看到的报错里那句关键提示:
错误:找不到匹配的‘__invoke(std::shared_ptr
(&)(int&&, int&&), std::__tuple_element_t<0, std::tuple<int, int> >&, std::__tuple_element_t<1, std::tuple<int, int> >&)’函数调用
就是在说这个绑定失败的问题。
3. 怎么解决?用Lambda包裹就好啦
最通用的解决办法是用Lambda把std::make_shared包起来,让Lambda帮我们处理参数的完美转发,避免显式指定模板参数带来的签名冲突:
修改A类的get()方法成这样:
std::shared_ptr<Base> get() { return std::apply([](auto&&... args) { return std::make_shared<T>(std::forward<decltype(args)>(args)...); }, args_); }
这里Lambda的参数auto&&... args会自动适配std::apply传过来的参数类型(左值/右值都能搞定),然后通过std::forward把参数完美转发给std::make_shared,让std::make_shared自己推导模板参数,这样就不会有签名不匹配的问题了。
修改后的完整代码:
#include <iostream> #include <tuple> #include <utility> #include <memory> struct Base { virtual void foo() = 0; }; struct Test : Base { Test(int, int) {} void foo() override {std::cout << "Test\n";} }; struct Test2 : Base { Test2(int, int, const std::string&) {} void foo() override {std::cout << "Test2\n";} }; template <typename T, typename... Args> struct A { A(Args... args) : args_(std::make_tuple(args...)) {} std::shared_ptr<Base> get() { return std::apply([](auto&&... args) { return std::make_shared<T>(std::forward<decltype(args)>(args)...); }, args_); } std::tuple<Args...> args_; }; template <typename T, typename... Args> auto make_A(Args... args) { return A<T, Args...>(args...); } int main(int argc, char** argv) { make_A<Test>(1, 2).get()->foo(); make_A<Test2>(1, 2, "").get()->foo(); }
这段代码在GCC 9下能正常编译运行,输出:
Test Test2
补充:另一种不太灵活的解决办法
如果你非要显式指定std::make_shared的模板参数,可以把参数改成左值引用:
std::shared_ptr<Base> get() { return std::apply(std::make_shared<T, Args&...>, args_); }
这样实例化后的std::make_shared签名是std::shared_ptr<Test>(int&, int&),能匹配std::apply传递的左值引用。但这种方式只能处理左值情况,不如Lambda的方式灵活,Lambda能完美适配所有值类型(左值、右值、将亡值)。
内容的提问来源于stack exchange,提问作者Dmitry

