26-6 指针的部分模板特化

在上一课26.4——类模板特化中,我们研究了一个简单的模板Storage类,以及一个针对特定类型的特化double:

#include <iostream>

template <typename T>
class Storage
{
private:
    T m_value {};
public:
    Storage(T value)
      : m_value { value }
    {
    }

    void print()
    {
        std::cout << m_value << '\n';
    }
};

template<>
void Storage<double>::print() // fully specialized for type double
{
    std::cout << std::scientific << m_value << '\n';
}

int main()
{
    // Define some storage units
    Storage i { 5 };
    Storage d { 6.7 }; // will cause Storage<double> to be implicitly instantiated

    // Print out some values
    i.print(); // calls Storage<int>::print (instantiated from Storage<T>)
    d.print(); // calls Storage<double>::print (called from explicit specialization of Storage<double>::print())
}

img

然而,尽管这个类很简单,但它却有一个隐藏的缺陷:当T是指针类型时,它虽然可以编译,但会出错。例如:

int main()
{
    double d { 1.2 };
    double *ptr { &d };

    Storage s { ptr };
    s.print();

    return 0;
}

在作者的电脑上,结果如下:

img

发生了什么?因为ptr是一个double,s其类型为 Storage<double*>,这意味着m_value 的类型为 double。当构造函数被调用时,m_value会收到一个持有的地址的副本,而当成员函数print()被调用ptr时,打印的正是这个地址。

那么我们该如何解决这个问题呢?

一种方法是为类型添加完整的特化double*:

#include <iostream>

template <typename T>
class Storage
{
private:
    T m_value {};
public:
    Storage(T value)
      : m_value { value }
    {
    }

    void print()
    {
        std::cout << m_value << '\n';
    }
};

template<>
void Storage<double*>::print() // fully specialized for type double*
{
    if (m_value)
        std::cout << std::scientific << *m_value << '\n';
}

template<>
void Storage<double>::print() // fully specialized for type double (for comparison, not used)
{
    std::cout << std::scientific << m_value << '\n';
}

int main()
{
    double d { 1.2 };
    double *ptr { &d };

    Storage s { ptr };
    s.print(); // calls Storage<double*>::print()

    return 0;
}

现在打印出的结果是正确的:

img

但这只解决了指针类型Tdouble*时的问题。如果指针类型Tint*,char*或任何其他指针类型呢?

我们真的不想为每种指针类型都创建一个完整的特化版本。事实上,这根本不可能,因为用户总是可以传入指向程序自定义类型的指针。

指针的部分模板特化

您可以考虑尝试创建一个类型重载的模板函数T*:

// doesn't work
template<typename T>
void Storage<T*>::print()
{
    if (m_value)
        std::cout << std::scientific << *m_value << '\n';
}

img

这样的函数是一个部分特化的模板函数,因为它限制了类型T(指针类型),但T它仍然是一个类型模板参数。

遗憾的是,这样做行不通,原因很简单:截至撰写本文时(C++23),函数不能进行部分特化。正如我们在第26.5 课——部分模板特化中提到的,只有类才能进行部分特化。

所以,我们不妨对该Storage类进行部分特例化:

#include <iostream>

template <typename T>
class Storage // This is our primary template class (same as previous)
{
private:
    T m_value {};
public:
    Storage(T value)
      : m_value { value }
    {
    }

    void print()
    {
        std::cout << m_value << '\n';
    }
};

template <typename T> // we still have a type template parameter
class Storage<T*> // This is partially specialized for T*
{
private:
    T* m_value {};
public:
    Storage(T* value)
      : m_value { value }
    {
    }

    void print();
};

template <typename T>
void Storage<T*>::print() // This is a non-specialized function of partially specialized class Storage<T*>
{
    if (m_value)
        std::cout << std::scientific << *m_value << '\n';
}

int main()
{
    double d { 1.2 };
    double *ptr { &d };

    Storage s { ptr }; // instantiates Storage<double*> from partially specialized class
    s.print(); // calls Storage<double*>::print()

    return 0;
}

img

我们Storage<T>::print()在类外部定义了这个函数,只是为了演示如何实现,并表明该定义与Storage<T>::print()上面那个无法正常工作的部分特化函数完全相同。然而,现在它不再Storage<T>是一个部分特Storage<T>::print()化的类,而是一个非特化函数,因此它是被允许的。

值得注意的是,我们的类型模板参数定义为T,而不是T。这意味着 T将被推导出为非指针类型,因此我们需要在任何需要指向T的指针的地方使用T。另外需要提醒的是,部分特化Storage<T*>需要在主模板类之后定义Storage

所有权和生命周期问题

上述部分特化的类Storage<T*>还存在另一个潜在问题。因为m_value是一个指针T*,它指向传入的对象。如果该对象随后被销毁,我们的 Storage<T*> 就会悬空。

核心问题在于,我们的实现一方面Storage具有复制语义(意味着它会复制自身的初始化器),另一方面又Storage<T*>具有引用语义(意味着它是对其初始化器的引用)。这种不一致极易导致 bug。

我们可以用几种不同的方法来处理这类问题(按复杂程度递增的顺序排列):

  • 务必明确指出这Storage<T>是一个视图类(带有引用语义),因此调用者有责任确保被指向的对象在与主模板类相同的生命周期内保持有效Storage<T>。遗憾的是,由于这个部分特化的类必须与主模板类同名,我们不能给它起一个类似 <view> 的名字StorageView。所以我们只能使用注释或其他可能被忽略的方法。这不是一个理想的选择。
  • 完全禁止使用Storage<T>。我们可能根本不需要Storage<T>它,因为调用者总可以在实例化时解引用指针来使用Storage并复制该值(这对于存储类来说在语义上是合适的)。
    然而,虽然你可以删除重载函数,但 C++(从 C++23 开始)不允许你删除类。显而易见的解决方案是进行部分特化,然后在模板实例化时Storage<T>采取一些措施使其无法编译(例如),但这种方法有一个主要的缺点:std::nullptr_t不是指针类型,因此Storage< std::nullptr_t>无法匹配Storage<T>!

更好的解决方案是完全避免部分特化,并在主模板上使用static_assert来确保T它是我们认可的类型。以下是这种方法的示例:

#include <iostream>
#include <type_traits> // for std::is_pointer_v and std::is_null_pointer_v

template <typename T>
class Storage
{
    // Make sure T isn't a pointer or a std::nullptr_t
    static_assert(!std::is_pointer_v<T> && !std::is_null_pointer_v<T>, "Storage<T*> and Storage<nullptr> disallowed");

private:
    T m_value {};

public:
    Storage(T value)
      : m_value { value }
    {
    }

    void print()
    {
        std::cout << m_value << '\n';
    }
};

int main()
{
    double d { 1.2 };

    Storage s1 { d }; // ok
    s1.print();

    Storage s2 { &d }; // static_assert because T is a pointer
    s2.print();

    Storage s3 { nullptr }; // static_assert because T is a nullptr
    s3.print();

    return 0;
}

img

  • 需要Storage<T*>在堆上复制该对象。如果您自己管理所有堆内存,则需要重载构造函数、复制构造函数、复制赋值和析构函数。一个更简单的替代方法是直接使用 std::unique_ptr(我们将在第 22.5 课中介绍):
#include <iostream>
#include <type_traits> // for std::is_pointer_v and std::is_null_pointer_v
#include <memory>

template <typename T>
class Storage
{
    // Make sure T isn't a pointer or a std::nullptr_t
    static_assert(!std::is_pointer_v<T> && !std::is_null_pointer_v<T>, "Storage<T*> and Storage<nullptr> disallowed");

private:
    T m_value {};

public:
    Storage(T value)
      : m_value { value }
    {
    }

    void print()
    {
        std::cout << m_value << '\n';
    }
};

template <typename T>
class Storage<T*>
{
private:
    std::unique_ptr<T> m_value {}; // use std::unique_ptr to automatically deallocate when Storage is destroyed

public:
    Storage(T* value)
      : m_value { std::make_unique<T>(value ? *value : 0) } // or throw exception when !value
    {
    }

    void print()
    {
        if (m_value)
            std::cout << *m_value << '\n';
    }
};

int main()
{
    double d { 1.2 };

    Storage s1 { d }; // ok
    s1.print();

    Storage s2 { &d }; // ok, copies d on heap
    s2.print();

    return 0;
}

使用部分模板类特化来创建类的指针和非指针实现非常有用,尤其是在您希望一个类以不同的方式处理指针和非指针,但又希望这种处理方式对最终用户完全透明时。

posted @ 2025-11-30 00:39  游翔  阅读(7)  评论(0)    收藏  举报