13.More C++ Idioms - Coercion by Member Template惯用法 and more

enter image description here

继续讨论C++ idioms wikibooke这本书, Coercion by Member Template惯用法,意图是To increase the flexibility of a class template's interface by allowing the class template to participate in the same implicit type conversions (coercion) as its parameterizing types enjoy., 举例说明?


Coercion by Member Template(通过成员模板进行类型转换) 这个 C++ 惯用法的目的是 增强类模板接口的灵活性,使其能够与参数化类型(template parameters)享受相同的隐式类型转换(coercion)。具体来说,当类模板的参数化类型支持某种类型转换时,该类模板的对象也应该支持类似的转换,以提供更直观、灵活的接口。

简单讲就是如果T可以隐式转化为U,那么 smartptr<T>可以转化成smartptr<U>

示例 1:智能指针的隐式转换

一个典型的应用场景是智能指针(类似 std::shared_ptrstd::unique_ptr),当我们有一个 SmartPtr<T> 时,如果 T 可以转换为 U,那么 SmartPtr<T> 也应该可以转换为 SmartPtr<U>

示例代码


#include <iostream> template <typename T> class SmartPtr { T* ptr; public: explicit SmartPtr(T* p = nullptr) : ptr(p) {} ~SmartPtr() { delete ptr; } // 拷贝构造(用于相同类型) SmartPtr(const SmartPtr& other) : ptr(other.ptr ? new T(*other.ptr) : nullptr) {} // 允许从 SmartPtr<U> 转换到 SmartPtr<T>,如果 U* 能隐式转换为 T* template <typename U> SmartPtr(const SmartPtr<U>& other) : ptr(other.ptr ? new T(*other.ptr) : nullptr) {} // 访问底层指针 T* get() const { return ptr; } void show() const { std::cout << "SmartPtr holds: " << *ptr << std::endl; } }; class Base { public: virtual void show() const { std::cout << "Base class" << std::endl; } virtual ~Base() = default; }; class Derived : public Base { public: void show() const override { std::cout << "Derived class" << std::endl; } }; int main() { SmartPtr<Derived> derivedPtr(new Derived); derivedPtr.show(); // 使用成员模板,实现从 SmartPtr<Derived> 到 SmartPtr<Base> 的隐式转换 SmartPtr<Base> basePtr = derivedPtr; basePtr.show(); return 0; }

分析 为什么需要成员模板?

SmartPtr<T> 可能需要从 SmartPtr<U> 隐式转换(如果 U* 可以转换为 T)。 例如,Derived 可以隐式转换为 Base*,因此 SmartPtr<Derived> 也应该可以转换为 SmartPtr<Base>。 如何实现?

定义 template <typename U> SmartPtr(const SmartPtr<U>& other); 该构造函数仅在 U* 能够隐式转换为 T* 时有效。 示例效果

SmartPtr<Derived> 能够隐式转换为 SmartPtr<Base>,正如原始指针 Derived* 能隐式转换为 Base* 一样。

示例 2:数学向量类 另一个例子是一个简单的 Vector<T> 类,它应该允许 Vector<int> 自动转换为 Vector<double>,就像 int 可以转换为 double 一样。

#include <iostream>

template <typename T>
class Vector {
    T x, y;

public:
    Vector(T x, T y) : x(x), y(y) {}

    // 允许从 Vector<U> 隐式转换为 Vector<T>,前提是 U 能转换为 T
    template <typename U>
    Vector(const Vector<U>& other) : x(other.getX()), y(other.getY()) {}

    T getX() const { return x; }
    T getY() const { return y; }

    void show() const { std::cout << "Vector(" << x << ", " << y << ")" << std::endl; }
};

int main() {
    Vector<int> vi(3, 4);
    vi.show();

    // 由于成员模板的存在,这里可以隐式转换为 Vector<double>
    Vector<double> vd = vi;
    vd.show();

    return 0;
}

总结 “Coercion by Member Template” 通过 成员模板构造函数 允许类模板的对象 享受其参数类型的隐式转换,从而:

使智能指针(SmartPtr<T>)可以像普通指针一样转换。 使数学向量(Vector<T>)在不同数值类型之间转换,提升泛型编程的便利性。 这个惯用法在 智能指针、容器、数学运算类 等模板类设计中非常常见。


||=== Build: Debug in Coercion by Member Template (compiler: GNU GCC Compiler) ===| C:\workspace\cpp-exercises\Coercion by Member Template\main.cpp||In instantiation of 'void SmartPtr::show() const [with T = Derived]':| C:\workspace\cpp-exercises\Coercion by Member Template\main.cpp|40|required from here| C:\workspace\cpp-exercises\Coercion by Member Template\main.cpp|24|error: no match for 'operator<<' (operand types are 'std::basic_ostream' and 'Derived')|


你的编译错误是由于 std::ostream 不能直接输出 Derived 类型的对象。在 SmartPtr::show() 方法中,我们尝试使用 std::cout << *ptr;,但 Derived 类没有重载 operator<<,导致编译错误。

解决方案 我们有两种方式来修复这个问题: 方法 1:为 Base 和 Derived 类提供 operator<< 重载

#include <iostream>

template <typename T>
class SmartPtr {
    T* ptr;

public:
    explicit SmartPtr(T* p = nullptr) : ptr(p) {}

    ~SmartPtr() { delete ptr; }

    // 允许从 SmartPtr<U> 转换到 SmartPtr<T>,如果 U* 能隐式转换为 T*
    template <typename U>
    SmartPtr(const SmartPtr<U>& other) : ptr(other.get() ? new T(*other.get()) : nullptr) {}

    // 访问底层指针
    T* get() const { return ptr; }

    void show() const { 
        if (ptr)
            std::cout << *ptr << std::endl; 
        else
            std::cout << "SmartPtr is empty" << std::endl;
    }
};

// 定义基类
class Base {
public:
    virtual void show() const { std::cout << "Base class" << std::endl; }
    virtual ~Base() = default;

    // 重载 << 让 std::cout 能输出 Base 或其派生类
    friend std::ostream& operator<<(std::ostream& os, const Base& obj) {
        obj.show();
        return os;
    }
};

// 定义派生类
class Derived : public Base {
public:
    void show() const override { std::cout << "Derived class" << std::endl; }
};

int main() {
    SmartPtr<Derived> derivedPtr(new Derived);
    derivedPtr.show();

    // SmartPtr<Derived> 能够转换为 SmartPtr<Base>
    SmartPtr<Base> basePtr = derivedPtr;
    basePtr.show(); 

    return 0;
}

方法 2:直接调用 ptr->show() 如果你只想让 SmartPtr::show() 调用 T 自己的 show() 方法,而不是依赖 operator<<,可以这样修改:

void show() const { 
    if (ptr)
        ptr->show();  // 直接调用 T 的 show() 方法
    else
        std::cout << "SmartPtr is empty" << std::endl;
}

这样就不需要重载 operator<< 了。

总结

你的编译错误的原因是 std::cout << *ptr; 需要 operator<< 支持,而 Derived 没有实现它。解决方案:

重载 operator<<(推荐) 直接调用 ptr->show()(更简单但更局限) 根据你的需求选择合适的解决方案!

重点解释一下 // SmartPtr 能够转换为 SmartPtr SmartPtr basePtr = derivedPtr; 这两个模板类是如何实现类型转化得?


SmartPtr<Derived> 隐式转换为 SmartPtr<Base> 的过程中,主要依赖 成员模板(member template)构造函数,它允许 SmartPtr<T> 之间进行类型转换,只要 T* 可以隐式转换为 U*。


代码分析

// 允许从 SmartPtr<U> 转换到 SmartPtr<T>,如果 U* 能隐式转换为 T*
template <typename U>
SmartPtr(const SmartPtr<U>& other) : ptr(other.get() ? new T(*other.get()) : nullptr) {}

  1. 代码作用 这是 一个模板构造函数,它使得 SmartPtr<U> 可以转换为 SmartPtr<T>。 其 前提 是 U* 可以隐式转换为 T*,即 U 是 T 的派生类时才成立。 转换过程 假设:
SmartPtr<Derived> derivedPtr(new Derived);
SmartPtr<Base> basePtr = derivedPtr;  // 发生类型转换

我们来分解 SmartPtr<Base> basePtr = derivedPtr; 这句代码的转换过程。

  1. SmartPtr<Base> 需要调用合适的构造函数 derivedPtrSmartPtr<Derived> 类型 basePtr 需要的是 SmartPtr<Base> 由于 SmartPtr 提供了成员模板构造函数:
template <typename U>
SmartPtr(const SmartPtr<U>& other);

可以匹配 U = Derived,T = Base。

  1. 模板构造函数的匹配 展开后,相当于调用:
SmartPtr<Base>::SmartPtr<Derived>(const SmartPtr<Derived>& other);

这意味着:

other.get() 返回 Derived* 指针 new T(*other.get()) 变成 new Base(*derivedPtr.get()) 这里假设 Base 具有 Derived 的拷贝构造能力(或者是 slicing) ptr 被赋值为新创建的 Base*

  1. U* 到 T* 的隐式转换 如果 U 是 T 的派生类(即 Derived 继承自 Base),则 U* 可以隐式转换为 T*:
Derived* d = new Derived();
Base* b = d;  // 这里发生了指针转换

因此,SmartPtr<Derived> 也能转换为 SmartPtr<Base>,只要 我们在 SmartPtr 内部正确处理了 T* 的构造。

总结 关键点:

成员模板构造函数:

     template <typename U> SmartPtr(const SmartPtr<U>& other);

允许 SmartPtr<U> 转换为 SmartPtr,前提是 U* 可以隐式转换为 T*。 依赖指针转换:

由于 Derived* 可以转换为 Base,所以 SmartPtr<Derived> 可以转换为 SmartPtr<Base>。 在 SmartPtr 内部创建新的 T 对象:

ptr = new T(*other.get());

这确保了新 SmartPtr<Base> 仍然拥有自己的 Base* 资源。 这样,我们就成功 让 SmartPtr 可以隐式转换为 SmartPtr<Base> 了!

允许 SmartPtr<U> 转换为 SmartPtr<T>,前提是 U* 可以隐式转换为 T*,这前提是不是必须得,是否 U可以转化为T也行?


SmartPtr<U> 转换为 SmartPtr<T> 时,U 能隐式转换为 T 这一前提是 必要的,但并不是唯一可能的条件。理论上,如果 U 可以转换为 T(即存在 U → T 的转换),而不仅仅是 U* → T*,那么 SmartPtr<U> 仍然可能转换为 SmartPtr<T>,但实现方式会有所不同。


两种可能的转换方式 指针类型转换(U → T)** → 适用于 继承关系 值类型转换(U → T) → 适用于 存在用户定义转换 1. 指针类型转换(U → T)** 这种情况发生在 U 继承自 T 时,例如:

 class Base { };
    class Derived : public Base { };

对于 SmartPtr<Derived> 转换为 SmartPtr<Base>,只要 Derived* 能够隐式转换为 Base*,成员模板就能生效:

template <typename U>
SmartPtr(const SmartPtr<U>& other) : ptr(other.get() ? new T(*other.get()) : nullptr) {}

这里 T = Base,U = Derived,指针的转换 Derived* → Base* 天然支持,因此可以成功编译。

这就是标准的「指针向上转型(upcasting)」,广泛适用于 继承体系。

  1. 值类型转换(U → T) 如果 U 不能 直接隐式转换为 T*,但可以 转换为 T 的对象,那么仍然可以让 SmartPtr<U> 转换为 SmartPtr<T>,但需要修改 SmartPtr 的实现。例如:

    class Meter { public: explicit Meter(double v) : value(v) {} double getValue() const { return value; } private: double value; };

    class Centimeter { public: explicit Centimeter(double v) : value(v) {}

    // 用户定义转换 Centimeter → Meter
    operator Meter() const { return Meter(value / 100.0); }
    
    double getValue() const { return value; }
    

    private: double value; };

Centimeter 提供了 operator Meter(),因此 可以转换为 Meter,但 Centimeter* 不能转换为 Meter*,因为两者是不同的类,没有继承关系。

如果 SmartPtr<U> 采用如下实现:

template <typename U>
SmartPtr(const SmartPtr<U>& other) : ptr(other.get() ? new T(*other.get()) : nullptr) {}

那么:

SmartPtr<Centimeter> cmPtr(new Centimeter(150));
SmartPtr<Meter> mPtr = cmPtr;  // 依赖 Centimeter → Meter 的转换

在 SmartPtr 内部,other.get() 是 Centimeter,它可以 显式或隐式转换为 Meter,因此 new T(other.get()) 变成了:

new Meter(static_cast<Meter>(*other.get()))

这样,即使 指针不能转换,但由于 U → T 存在转换,也可以完成 SmartPtr → SmartPtr 的转换。

结论 必须满足至少一种转换条件 如果 U* 可以转换为 T(即 U 继承自 T),那么 SmartPtr<U> 自然能转换 为 SmartPtr<T>,无需额外处理。 如果 U 可以转换为 T(如 Centimeter → Meter),那么 SmartPtr<U> 仍然可以转换为 SmartPtr<T>,但 SmartPtr 的实现需要 new T(other.get()) 这种方式来完成转换。 总结 在 SmartPtr<U> 转换为 SmartPtr<T> 的过程中,U* 能隐式转换为 T* 并不是唯一的必要条件,但至少需要:

U*T*(指针转换):常见于继承关系 UT(值转换):常见于用户定义转换 如果 U* 不能转换为 T*,但 U 可以转换为 T,则 SmartPtr<U> 仍然可以转换为 SmartPtr<T>,但 SmartPtr 需要支持 new T(*other.get()) 这种方式。

评论