Skip to content

19.2 类模板实例

By Alex on April 29th, 2008 | last modified by Alex on December 21st, 2020

翻译 By dashjay 2020-12-26 | last modified by 2020-12-26

模板函数在 C++ 中的实现是一件很有值得的事情,因为未来的课程将会在这些基于这些概念。事实证明 C++ 不会直接直接编译函数。相反,在编译的时,当编译器遇到一个模板函数的时候,他会复制模板函数并且替换模板类参数为实际类型。带有实际类型的函数被称为 函数模板实例

让我们看一个简单的例子,首先,我们写了一个模板函数:

template <typename T> // this is the template parameter declaration
const T& max(const T& x, const T& y)
{
    return (x > y) ? x : y;
}

当编译你的程序的时候,如果编译器遇到一个模板函数的调用:

int i{ max(3, 7) }; // calls max(int, int)

编译器说,“好吧,我们想要调用 max(int, int)”。编译器拷贝了函数模板,然后创建模板实例 max(int, int)

const int& max(const int &x, const int &y)
{
    return (x > y) ? x : y;
}

这现在是一个 “正常的函数” 可以被编译成机器语言的。

现在,让我们在稍后的代码中,你再次调用了 max() 使用不同的类型:

double d{ max(6.34, 18.523) }; // calls max(double, double)

C++ 为 max(double, double) 自动的创建了一个模板实例:

const double& max(const double &x, const double &y)
{
    return (x > y) ? x : y;
}

并且然后编译它。

编译器足够聪明,知道它只需要为每组唯一类型参数(每个文件)创建一个模板实例。也值得注意如果你创建了一个模板函数但是没有调用它,没有模板实例会被创建。

Operators, function calls, and function templates

操作符,函数调用和函数模板

有一个前提,模板函数会既能和内置的类型工作(例如:char,int,double 等等)和类。当编译器编译模板实例的时候,它编译就像普通函数那样。在普通函数中,必须定义与类型一起使用的任何运算符或函数调用,否则你会得到编译错误。类似地,模板函数中的任何运算符或函数调用都必须为实例化函数模板的任何类型定义。让我们更详细地看看这个例子。

首先,我们能创建一个简单的类:

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }
};

现在,让我们看一下当我们尝试使用 Cents 类来调用我们的模板函数 max()

template <typename T> // this is the template parameter declaration
const T& max(const T& x, const T& y)
{
    return (x > y) ? x : y;
}

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }
};

int main()
{
    Cents nickle{ 5 };
    Cents dime{ 10 };

    Cents bigger{ max(nickle, dime) };

    return 0;
}

C++ 会创建一个 max() 像这样的模板实例:

const Cents& max(const Cents &x, const Cents &y)
{
    return (x > y) ? x : y;
}

然后紧接着他会尝试编译这个函数,看到问题了么?C++ 不能评估 x > y,因为 x 和 y 都是 Cents 类对象,而且不能知道如何编译他们。因此,这会产生一个很难看的编译错误,像这样:

1>c:\consoleapplication1\main.cpp(4): error C2676: binary '>': 'const Cents' does not define this operator or a conversion to a type acceptable to the predefined operator
1>  c:\consoleapplication1\main.cpp(23): note: see reference to function template instantiation 'const T &max(const T &,const T &)' being compiled
1>          with
1>          [
1>              T=Cents
1>          ]

最上方的错误信息指出没有Cents这个类的 > 操作符重载。底部的错误指出模板函数调用报错了,以及模板化参数的类型。

想要解决这个问题,简单的给每个你想要使用 max() 的任何类重载 > 操作符:

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }

    friend bool operator>(const Cents &c1, const Cents &c2)
    {
        return (c1.m_cents > c2.m_cents);
    }
};

现在 C++ 能知道如何比较 x > y 当 x 和 y 是 Cents!结果,我们的 max() 现在可以和两个 Cents 类的两个对象。

另一个例子

让我们在看一个函数模板的例子。接下来的函数模板将会计算数组中对象的平均值:

template <class T>
T average(T *array, int length)
{
    T sum(0);

    for (int count{ 0 }; count < length; ++count)
        sum += array[count];

    sum /= length;
    return sum;
}

让我们来看一下实际使用:

#include <iostream>

template <class T>
T average(T *array, int length)
{
    T sum(0);
    for (int count{ 0 }; count < length; ++count)
        sum += array[count];

    sum /= length;
    return sum;
}

int main()
{
    int array1[]{ 5, 3, 2, 1, 4 };
    std::cout << average(array1, 5) << '\n';

    double array2[]{ 3.12, 3.45, 9.23, 6.34 };
    std::cout << average(array2, 4) << '\n';

    return 0;
}

结果产生了值:

3
5.535

如你所见,他能很好在内置的类型上运行!

值得注意的是,因为返回类型与数组元素是相同的模板化类型,做整型平均值会产生一个整型结果(丢弃任何)。这与整数除法产生整数结果的方式类似。我们将事物定义为这样是可以工作的,但是这可能是不可预期的,所以在这里写一些注释给类的用户也挺好。

现在当我们使用在 Cents 类上调用这个函数的时候,让我们来看看会发生什么:

#include <iostream>

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }

    friend bool operator>(const Cents &c1, const Cents &c2)
    {
        return (c1.m_cents > c2.m_cents);
    }
};

template <class T>
T average(T *array, int length)
{
    T sum(0);
    for (int count{ 0 }; count < length; ++count)
        sum += array[count];

    sum /= length;
    return sum;
}

int main()
{
    Cents array3[]{ Cents(5), Cents(10), Cents(15), Cents(14) };
    std::cout << average(array3, 4) << '\n';

    return 0;
}

编译器陷入疯狂报了一堆错!

example.cpp

(33): error C2679: binary '<<': no operator found which takes a right-hand operand of type 'T' (or there is no acceptable conversion)

        with

        [

            T=Cents

        ]

C:/data/msvc/14.22.27905/include\ostream(437): note: could be 'std::basic_ostream> &std::basic_ostream>::operator <<(std::basic_streambuf> *)'
C:/data/msvc/14.22.27905/include\ostream(412): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(const void *)'
C:/data/msvc/14.22.27905/include\ostream(394): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(long double)'
C:/data/msvc/14.22.27905/include\ostream(376): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(double)'
C:/data/msvc/14.22.27905/include\ostream(358): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(float)'
C:/data/msvc/14.22.27905/include\ostream(340): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(unsigned __int64)'
C:/data/msvc/14.22.27905/include\ostream(322): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(__int64)'
C:/data/msvc/14.22.27905/include\ostream(304): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(unsigned long)'
C:/data/msvc/14.22.27905/include\ostream(286): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(long)'
C:/data/msvc/14.22.27905/include\ostream(268): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(unsigned int)'
C:/data/msvc/14.22.27905/include\ostream(248): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(int)'
C:/data/msvc/14.22.27905/include\ostream(230): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(unsigned short)'
C:/data/msvc/14.22.27905/include\ostream(202): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(short)'
C:/data/msvc/14.22.27905/include\ostream(184): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(bool)'
C:/data/msvc/14.22.27905/include\ostream(179): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(std::ios_base &(__cdecl *)(std::ios_base &))'
C:/data/msvc/14.22.27905/include\ostream(174): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(std::basic_ios> &(__cdecl *)(std::basic_ios> &))'
C:/data/msvc/14.22.27905/include\ostream(169): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(std::basic_ostream> &(__cdecl *)(std::basic_ostream> &))'
C:/data/msvc/14.22.27905/include\ostream(613): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const char *)'
C:/data/msvc/14.22.27905/include\ostream(658): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,char)'
C:/data/msvc/14.22.27905/include\ostream(694): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const char *)'
C:/data/msvc/14.22.27905/include\ostream(739): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,char)'
C:/data/msvc/14.22.27905/include\ostream(858): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const signed char *)'
C:/data/msvc/14.22.27905/include\ostream(864): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,signed char)'
C:/data/msvc/14.22.27905/include\ostream(870): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const unsigned char *)'
C:/data/msvc/14.22.27905/include\ostream(876): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,unsigned char)'
C:/data/msvc/14.22.27905/include\ostream(931): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const std::error_code &)'
(33): note: while trying to match the argument list '(std::ostream, T)'

        with

        [

            T=Cents

        ]

Compiler returned: 2

记得我说过的疯狂报错信息么?We hit the motherlode!尽管看起来很吓人,这些实际上相当直接。第一行告诉你不能在 Cents 类中找到一个重载的 << 操作符。中间是所有想要用来匹配的函数,但是都失败了。最后一个错误指出了产生这一堆错的的函数调用。

记住 average() 返回了一个 Cents 对象,并且我们正在尝试去输出那个对象到 std::cout 使用 << 操作符。然而,我们已经为我们的 Cents 类型定义了 << 从操作符。让我们那样做:

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }

    friend bool operator>(const Cents &c1, const Cents &c2)
    {
        return (c1.m_cents > c2.m_cents);
    }

    friend std::ostream& operator<< (std::ostream &out, const Cents &cents)
    {
        out << cents.m_cents << " cents ";
        return out;
    }
};

如果我们再次编译,我们就会得到另一个错误:

c:test.cpp(14) : error C2676: binary '+=' : 'Cents' does not define this operator or a conversion to a type acceptable to the predefined operator

这个错误实际上是当我们调用 average(Cents* int) 的时候创建的函数模板实例造成的。记住当我们调用一个模板函数的时候,编译器使用漏字板可出一个函数的拷贝,并且模板类型参数(占位符类型)已经被替换为实际上函数调用时的类型。这是一个函数模板实例化时,当 T 替换为一个 Cents 对象时,再调用 average()的例子:

template <class T>
Cents average(Cents *array, int length)
{
    Cents sum(0);
    for (int count{ 0 }; count < length; ++count)
        sum += array[count];

    sum /= length;
    return sum;
}

我们得到错误信息的原因是因为下面这行:

        sum += array[count];

在这个例子当中,sum 是一个 Cents 对象,但是我们为 Cents 对象去没有定义 += 操作符!为了让 average() 能在 Cents 类下调用,我们需要去定义这个函数。往后看,我们可以看到 average() 也使用了 /= 操作符,因此我们也要继续定义:

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }

    friend bool operator>(const Cents &c1, const Cents &c2)
    {
        return (c1.m_cents > c2.m_cents);
    }

    friend std::ostream& operator<< (std::ostream &out, const Cents &cents)
    {
        out << cents.m_cents << " cents ";
        return out;
    }

    Cents& operator+=(const Cents &cents)
    {
        m_cents += cents.m_cents;
        return *this;
    }

    Cents& operator/=(int value)
    {
        m_cents /= value;
        return *this;
    }
};

最终我们的代码会编译并且运行!这是结果:

11 cents

如果这看起来很费事,那只是因为我们的 Cents 类是一个如此基础的开始。这里的关键点是实际上我们根本没有必要去修改 average() 来保证它在 Cents 类下工作(或者为任何其他类型)。我们简单地为 Cents 类定义实现 average() 要用到的操作符,然后编译器做了剩下部分的工作!