48. 如何编写一个支持完美转发的包装器(Wrapper)类?
包装器类,说白了就是给现有类型「套个壳」。你想想看,有时候我们需要给某个对象增加日志、加锁、引用计数,或者单纯想隐藏实现细节——这时候包装器就派上用场了。
但问题来了:如果包装器不支持完美转发,那用户传个临时对象进来,你非得把它变成左值再传进去,性能就白丢了。我早年写过一个网络库的包装器,就因为没处理好转发,压测时性能直接掉了30%。嗯,从那以后我对完美转发就特别上心。
核心思路:模板构造函数 + 转发引用
要让包装器支持完美转发,关键就两步:
- 模板化构造函数:接受任意类型参数
- 使用转发引用:
T&&配合std::forward
来看一个最基础的例子:
template<typename T>
class Wrapper {
public:
// 模板构造函数,支持完美转发
template<typename U>
explicit Wrapper(U&& arg)
: data_(std::forward<U>(arg))
{
std::cout << "Wrapper constructed\n";
}
// 访问底层对象
T& get() { return data_; }
const T& get() const { return data_; }
private:
T data_;
};
这个包装器可以这样用:
std::string str = "hello";
Wrapper<std::string> w1(str); // 左值,拷贝构造
Wrapper<std::string> w2(std::move(str)); // 右值,移动构造
Wrapper<std::string> w3("temporary"); // 临时对象,直接移动
你看,三种情况都正确处理了。左值进来就拷贝,右值进来就移动,一次多余的拷贝都没有。
避坑指南:别让包装器自己「吃掉」转发
我曾经犯过一个低级错误——在构造函数里先取了个引用,再转发:
// 错误示范
template<typename U>
explicit Wrapper(U&& arg) {
U& ref = arg; // 这里把右值引用变成了左值引用
data_ = std::forward<U>(ref); // 转发失效!
}
为什么会这样?因为 ref 是个左值,std::forward 看到左值引用类型,就不会把它转成右值了。记住一个原则:转发引用只能转发一次,而且必须在它还是「未命名右值」的时候转发。
进阶:包装器内部的转发函数
有时候包装器不只是存对象,还要转发调用。比如我们写一个「带日志的调用包装器」:
template<typename Func>
class LoggedWrapper {
public:
template<typename F>
explicit LoggedWrapper(F&& f)
: func_(std::forward<F>(f)) {}
// 完美转发调用
template<typename... Args>
auto operator()(Args&&... args)
-> decltype(func_(std::forward<Args>(args)...))
{
std::cout << "Calling function with "
<< sizeof...(args) << " args\n";
return func_(std::forward<Args>(args)...);
}
private:
Func func_;
};
这个包装器可以这样用:
auto lambda = [](int a, std::string&& s) {
return a + s.size();
};
LoggedWrapper<decltype(lambda)> wrapper(std::move(lambda));
int result = wrapper(42, std::string("test")); // 完美转发
我个人习惯在写这种包装器时,用 decltype(auto) 做返回类型推导,代码更简洁:
template<typename... Args>
decltype(auto) operator()(Args&&... args) {
std::cout << "Calling...\n";
return func_(std::forward<Args>(args)...);
}
实战:带引用计数的包装器
我在一个内存池项目中写过这样的包装器——它包装任意对象,自动管理引用计数:
template<typename T>
class RefCountedWrapper {
public:
template<typename... Args>
explicit RefCountedWrapper(Args&&... args)
: ref_count_(1)
, data_(std::forward<Args>(args)...)
{
std::cout << "RefCountedWrapper created, ref=1\n";
}
// 禁止拷贝,只允许移动
RefCountedWrapper(RefCountedWrapper&& other) noexcept
: ref_count_(other.ref_count_)
, data_(std::move(other.data_))
{
other.ref_count_ = 0;
}
T& get() { return data_; }
int use_count() const { return ref_count_; }
private:
int ref_count_;
T data_;
};
这里有个细节:data_(std::forward<Args>(args)...) 用的是参数包展开,可以完美转发多个参数给底层对象的构造函数。比如:
struct Point {
Point(int x, int y) : x_(x), y_(y) {}
int x_, y_;
};
RefCountedWrapper<Point> wp(3, 4); // 完美转发两个int
知识结构图
下面这张图总结了编写完美转发包装器的核心流程:
总结几个要点
写支持完美转发的包装器,记住这几条就够了:
- 构造函数必须是模板,参数用转发引用
U&& - 初始化列表里用
std::forward,不要自己手动std::move - 转发引用只能转发一次,用完就废了
- 考虑异常安全:如果转发过程中抛异常,要保证包装器处于有效状态
- 注意 SFINAE:如果包装器需要限制类型,用
std::enable_if或 C++20 的requires
noexcept 判断。比如 noexcept(std::is_nothrow_constructible_v<T, U>),这样移动操作能获得更好的优化。
说实话,完美转发包装器写起来不难,但坑确实不少。我建议你从最简单的版本开始,逐步加功能,每加一个特性就写几个测试用例跑一跑。尤其是左值、右值、const 左值这三种情况,一定要覆盖到。
最后送你一句话:包装器写得好,性能差不了;转发没弄对,优化全白费。
公众号:蓝海资料掘金营,微信 deep3321