C++如何将底层类型的重载运算符应用于自定义包装类型
我有一个用于包装容器类的结构体:
struct ContainerWrapper { Container data; };
Container类型已经定义了大量重载运算符,例如operator+、operator*等。是否存在方法可以直接“继承”所有这些运算符,而无需手动逐一添加实现?换言之,我们是否可以避免编写如下所示的手动转发代码:
struct ContainerWrapper { auto operator+(const ContainerWrapper& other) const { return data + other.data; } Container data; };
转而通过定义ContainerWrapper与Container之间的某种隐式转换规则,使得两个ContainerWrapper类型的实例可以直接进行加法等运算,无需手动编写上述转发逻辑?
编辑补充
我尝试通过继承的方式解决该问题,但遇到了类型转换错误,以下是复现问题的示例代码:
#include <cstddef> #include <iostream> template <typename T, std::size_t N> struct Container { Container operator+(const Container& other) { Container res; for (std::size_t i = 0; i < N; ++i) { res.m_data[i] = m_data[i] + other.m_data[i]; } return res; } T& operator[](std::size_t i) { return m_data[i]; } const T& operator[](std::size_t i) const { return m_data[i]; } private: T m_data[N]; }; template <typename T, std::size_t N> struct ContainerWrapper : public Container<T, N> { ContainerWrapper() { for (std::size_t i = 0; i < N; ++i) { this->operator[](i) = static_cast<T>(i); } } }; int main() { using T = float; Container<T, 3> cont1{}; cont1[0] = 1.f; cont1[1] = 2.f; cont1[2] = 3.f; Container<T, 3> cont2{}; cont2[0] = 2.f; cont2[1] = 3.f; cont2[2] = 4.f; const auto cont3 = cont1 + cont2; std::cout << "cont1: " << cont1[0] << ", " << cont1[1] << ", " << cont1[2] << "\n"; std::cout << "cont2: " << cont2[0] << ", " << cont2[1] << ", " << cont2[2] << "\n"; std::cout << "cont3: " << cont3[0] << ", " << cont3[1] << ", " << cont3[2] << "\n"; ContainerWrapper<T, 3> contWrap1{}; ContainerWrapper<T, 3> contWrap2{}; const ContainerWrapper<T, 3> contWrap3 = contWrap1 + contWrap2; // 期望此处返回ContainerWrapper<T, 3>类型,但实际返回Container<T, 3>类型 return 0; }
上述代码无法通过编译,因为contWrap1 + contWrap2表达式的返回类型为Container<T, 3>,与期望的ContainerWrapper<T, 3>类型不匹配。
问题的核心原因是基类定义的运算符返回值固定为基类类型,派生类调用继承来的运算符时,不会自动将返回值转换为派生类类型。单纯依赖隐式转换走不通:C++最多只会执行一次用户自定义隐式转换,就算给ContainerWrapper加上到Container的隐式转换,让运算能匹配到Container的运算符,返回的Container类型也没法自动再转回ContainerWrapper。
不需要手动给每个运算符写完整转发逻辑,有两种可行方案:
方案1:CRTP奇异递归模板模式(零运行时开销,优先选择)
如果可以修改原Container的代码,用CRTP模式改造基类,让基类自动识别派生类类型,所有运算符一次编写,所有派生类/包装类自动复用,不需要重复写任何转发逻辑。
改造后的示例代码:
#include <cstddef> #include <iostream> // 基类新增CRTP模板参数,传入派生类类型 template <typename Derived, typename T, std::size_t N> struct ContainerBase { // 运算符直接返回派生类类型 Derived operator+(const Derived& other) const { Derived res; for (std::size_t i = 0; i < N; ++i) { res[i] = static_cast<const Derived*>(this)->operator[](i) + other[i]; } return res; } // 其余operator*、operator-、operator/等运算符都按这个逻辑在基类实现一次即可 }; // 普通Container将自身作为Derived参数传入基类 template <typename T, std::size_t N> struct Container : public ContainerBase<Container<T,N>, T, N> { T& operator[](std::size_t i) { return m_data[i]; } const T& operator[](std::size_t i) const { return m_data[i]; } private: T m_data[N]; }; // ContainerWrapper同样将自身作为Derived参数传入 template <typename T, std::size_t N> struct ContainerWrapper : public ContainerBase<ContainerWrapper<T,N>, T, N> { ContainerWrapper() { for (std::size_t i = 0; i < N; ++i) { (*this)[i] = static_cast<T>(i); } } T& operator[](std::size_t i) { return m_data[i]; } const T& operator[](std::size_t i) const { return m_data[i]; } private: T m_data[N]; }; int main() { using T = float; ContainerWrapper<T, 3> contWrap1{}; ContainerWrapper<T, 3> contWrap2{}; // 此时运算符返回值就是ContainerWrapper<T,3>,直接编译通过 const ContainerWrapper<T, 3> contWrap3 = contWrap1 + contWrap2; return 0; }
这个方案所有类型推导都在编译期完成,没有额外运行时开销,后续新增其他包装类时,只需要继承对应基类、实现自己的元素访问接口即可,不需要重复写任何运算符逻辑。
方案2:薄转发+隐式转换构造(无需修改原Container代码)
如果不能修改原有Container的实现,可以给ContainerWrapper增加两个能力:
- 支持从
Container实例隐式构造的转换函数 - 一层极薄的运算符转发,自动将Wrapper转为底层Container参与运算,再把结果隐式转回Wrapper
示例代码:
#include <cstddef> #include <iostream> // 原Container代码不需要做任何修改 template <typename T, std::size_t N> struct Container { Container operator+(const Container& other) { Container res; for (std::size_t i = 0; i < N; ++i) { res.m_data[i] = m_data[i] + other.m_data[i]; } return res; } T& operator[](std::size_t i) { return m_data[i]; } const T& operator[](std::size_t i) const { return m_data[i]; } private: T m_data[N]; }; template <typename T, std::size_t N> struct ContainerWrapper { ContainerWrapper() = default; // 隐式转换构造:从Container生成Wrapper实例 ContainerWrapper(const Container<T,N>& c) : data(c) {} // 隐式转换:Wrapper转为底层Container引用 operator Container<T,N>&() { return data; } operator const Container<T,N>&() const { return data; } // 薄转发层,每个运算符只需要写一行,不需要重复实现运算逻辑 friend ContainerWrapper operator+(const ContainerWrapper& lhs, const ContainerWrapper& rhs) { return static_cast<const Container<T,N>&>(lhs) + static_cast<const Container<T,N>&>(rhs); } // 其余operator*、operator-等运算符复制上面的结构,替换运算符即可 Container<T,N> data; }; int main() { using T = float; ContainerWrapper<T, 3> contWrap1{}; ContainerWrapper<T, 3> contWrap2{}; const ContainerWrapper<T, 3> contWrap3 = contWrap1 + contWrap2; return 0; }
这个方案对原有代码侵入性为零,虽然还是需要给每个运算符写转发,但转发逻辑只有一行,工作量远小于完整实现每个运算符的内部逻辑。
内容的提问来源于stack exchange,提问作者zhanginou

