兼容性和稳定性

C++11 与 C99 的兼容

C11 之前最新的 C标准是 1999 年制定的 C99 标准。而第一个 C++ 语言的标准却是在 1998 年(C++98),随后的 C++03 标准也只是对 C++98 进行了小的修正。这样一来,虽然C语言发展中大多数改进都被引入到了 C++ 的标准中,但是还是存在一些属于 C99 的"漏网之鱼".所以C++11将以下C99特性的支持也纳入了新标准中:

  • C99 中预定义宏
  • __fun__ 预定义标识符
  • _Pragma 标识符
  • 不定参数宏定义以及 __VA_ARGS__
  • 宽窄字符串连接

预定义宏

宏名称 功能描述
__STDC_HOSTED__ 如果编译器的目标系统环境中包含完整的C库,那么这个宏就定义为1
__STDC__ C编译器常用这个宏的值来表示编译器的实现是否和C一致。
__STDC_VERSION__ C编译器常用这个宏的值来表示编译器所支持的C标准的版本。比如:1999mmL
__STDC_ISO_10646__ 定义为一个 yyyymmL 格式的整数常量,比如 199712L,用来表示C++ 编译环境符合某个版本的 ISO/IEC 10646标准
#include 

int main()
{
    std::cout << "Standard Clib: " << __STDC_HOSTED__ << std::endl; // Standard Clib: 1
    std::cout << "Standard C: " << __STDC__ << std::endl; // Standard C:1
//    std::cout << "C Standard version: " << __STDC_VERSION__ << std::endl;
//    std::cout << "ISO/IEC " << __STDC_ISO_10646 << std::endl; // ISO/IEC 200009

    return 0;
}

__func__ 预定义标识符

#include 

const char* hello() { return __func__; }
const char* world() { return __func__; }

int main()
{
    std::cout << hello() << ", " << world() << std::endl; // hello, world
    return 0;
}
const char* hello() {
    static const char* __func__ = "hello";
    return __func__;
}
#include 

struct TestStruct
{
    TestStruct() : name(__func__) {}
    const char *name;
};
int main()
{
    TestStruct ts;
    std::cout << ts.name << std::endl;

    return 0;
}

__func__ 不可以做函数的默认参数

void FunFail(string func_name = __func__) {}; // error
// 由于在参数声明时,__func__ 还未被定义

_Pragma 操作符

#pragrma 是一条编译器预处理的指令(processor directive)。

#pragrma once

// 等同于
#ifndef THIS_HEADER
#define THIS_HEADER
// ...
#endif

c++11

_Pragma(字符串字面量)  // _Pragma 是一个操作符

_Pragma("once");

#define CONCAT(x) PRAGMA(concat on #x)
#define PRAGMA(x) _Pragma(#x)
CONCAT(../concat.dir)

// _Pragma(concat on "../concat.dir")
// #pragrma 不能在宏中展开

__VA_ARGS__

#define PR(...) printf(__VA_ARGS__)
#include 

#define LOG(...) {\
    fprintf(stderr, "%s: Line %d:\t", __FILE__, __LINE__); \
    fprintf(stderr, __VA_ARGS__); \
    fprintf(stderr, "\n"); \
}

int main()
{
    int x = 3;

    LOG("x = %d", x); // ../1/main.cpp: Line 13: x = 3

    return 0;
}

宽窄字符串的连接

之前,将 char 转成 wchar_t 是未定义的行为。

C++11,将char 和 wchar_t 进行连接时,编译器会将 char 转换成 wchar_t,然后进行连接.

long long 整型

C++11 相对于 C++98 在整型上最大的改变就是多了 long long。早在 1995 年,long long 就被提议写入 C++98 标准,但是 C++ 委员会拒绝了。后来,long long 却进入了 C99 标准,而且事实上也被很多编译器支持。

C++11要求,long long 在不同的平台上可以有不同的长度,但是至少 64 位。

  • LL (ll) ——> long long
  • ULL(ull, Ull, uLL) ——> unsigned long long
long long int lli = -9000000000000000000LL;
unsigned long long int ulli = -900000000000000000000ULL;

了解平台上 long long 大小的方法:

#include 
#include 

int main()
{
    std::cout << "[ " << LLONG_MIN << ", " << LLONG_MAX << " ]" << std::endl;  // [ -9223372036854775808, 9223372036854775807 ]
    std::cout << ULLONG_MAX << std::endl; // 18446744073709551615

    return 0;
}

扩展的整型

标准整型(standard interger type)

C++11 的 5 种标准的有符号整型

  • signed char
  • short int
  • int
  • long int
  • long long int

每种有符号整型都有对应的无符号整型.并且和对应整型的存储空间一致。

扩展整型(extended integer type)

C++11 对扩展整型的规定:有符号和无符号对应,且存储空间一致。

宏 __cplusplus

#indef __cplusplus
extern "C" {
#endif
// ...
#ifndef __cplusplus
}
#endif

extern "C" 可以抑制 C++ 对函数名,变量名等符号(symbol)进行重命名(name mangling),因此编译出的C目标文件和C++目标文件中的变量,函数名等符号都相同,链接器可以可靠地对两种类型的目标文件进行链接。

鉴于以上做法,程序员可能认为 __cplusplus 只有 “被定义” 和 “未定义” 两种状态。事实却非如此,__cplusplus 这个宏常被定义为一个整型值。随着标准的变化,__cplusplus 宏一般会是一个比以往标准中更大的值。

  • C++03 ——> __cplusplus == 199711L
  • C++11 ——> __cplusplus == 201103L
// 对 C++11 进行支持
#if __cplusplus < 201103L
#error "should use C++11 implementation"
#endif

静态断言

断言:运行时与预处理时

#include 
#include 

char *ArrayAlloc(int n)
{
    assert(n > 0);
    return new char[n];
}

int main()
{
    char* a = ArrayAlloc(0);

    return 0;
}

在 C++ 中,可以使用 NDEBUG 来禁用 assert .

#ifdef NDEBUG
#define assert(exptr)   (static_cast (0))
#else
...
#endif

静态断言与 static_assert

assert 只能在程序运行时起作用。

error 只能在编译器预处理时起作用

#include 
using namespace std;

// 枚举编译器对各种特性的支持,每个枚举值占一位
enum FeatureSupports {
    C99         =   0x0001,
    ExtInt      =   0x0002,
    SAssert     =   0x0004,
    NoExcept    =   0x0008,
    SMAX        =   0x0010,
};

// 一个编译器类型,包括名称,特性支持等
struct Compiler {
    const char * name;
    int spp;    // 使用FeatureSupports枚举
};

int main() {
    // 检查枚举值是否完备
    assert((SMAX - 1) == (C99 | ExtInt | SAssert | NoExcept));
    /*
     * assert 时运行时断言
    */

    Compiler a = {"abc", (C99 | SAssert)};
    // ...
    if (a.spp & C99) {
        // 一些代码...
    }
}
#include 
#include 
using namespace std;

template  int bit_copy(T& a, U& b){
    assert(sizeof(b) == sizeof(a));
    memcpy(&a,&b,sizeof(b));
};

int main() {
    int a = 0x2468;
    double b;
    bit_copy(a, b);
}

总而言之,我们想在编译时期就触发断言。

Boos 内置的 BOOST_STATIC_ASSERT 静态断言机制是利用 sizeof()操作符。

我们可以利用 “除0” 会导致编译器报错这个特性实现静态断言。

#define assert_static(e) \
    do { \
        enum { assert_static__ = 1 / (e) }; \
    } while (0)
#include 
using namespace std;

#define assert_static(e) \
    do { \
        enum { assert_static__ = 1/(e) }; \
    } while (0)

template  int bit_copy(T& a, U& b){
    assert_static(sizeof(b) == sizeof(a));
    memcpy(&a,&b,sizeof(b));
};

int main() {
    int a = 0x2468;
    double b = 2.0;
    bit_copy(a, b);
}

缺点:诊断信息不充分准确。

C++11 的 静态断言 static_assert。static_assert 接收2个参数,一个是断言表达式,这个表达式需要返回 bool 值;一个则是断言警告信息,通常是一个字符串。

#include 
#include 
using namespace std;

template  int bit_copy(T& a, U& b){
    static_assert(sizeof(b) == sizeof(a), "the parameters of bit_copy must have same width.");
    memcpy(&a,&b,sizeof(b));
};

int main() {
    int a = 0x2468;
    double b = 2.0;
    bit_copy(a, b);
}

static_assert 是编译时期的断言。

static_assert(sizeof(int) == 8, "This 64-bit machine should follow this!");

int main() { return 0; }
  • static_assert 接收的必须是常量表达式.【编译时期可以计算的表达式】
int positive(const int n) {
    static_assert(n > 0, "value must >0");
    /*
     * 涉及变量,应该使用 assert
    */
}

noexcept 修饰符 和 noexcept 操作符

断言是用于排除逻辑上不存在的状态。

异常时用于排除逻辑上的错误。

C++98 的异常处理:

void excpt_func() throw(int, double) { ... }

由于该特性很少被使用,C++11中被弃用了。表示函数不会抛出异常的动态异常声明 throw() 也被新的 noexcept 异常声明所取代。

noexcept 修饰函数,表示该函数不会抛出异常。不过与 throw() 动态异常声明不同的是,C++11 中如果 noexcept 修饰的函数抛出了异常,编译器可以选择直接调用 std::terminate() 函数来终止程序的运行,这比基于异常机制的 throw() 在效率上会高一些。这是因为异常机制会带来一些开销,比如函数抛出异常,会导致函数栈被依次地展开(unwind),并依帧调用在本帧中已构造的自动变量的析构函数等。

void excpt_func() noexcept;

void excpt_func() noexcept(常量表达式);
/*
 * 常量表达式会被转换成一个 bool 类型的值。
 * 如果为 ture,该函数不会抛出异常;反之,可能抛出异常。
*/
#include 
using namespace std;

void Throw() { throw 1; }

void NoBlockThrow() { Throw(); }

void BlockThrow() noexcept { Throw(); }

int main() {
    try {
        Throw();
    } catch(...) {
        cout << "Found throw." << endl;     // Found throw.
    }

    try {
        NoBlockThrow();
    } catch(...) {
        cout << "Throw is not blocked." << endl;    // Throw is not blocked.
    }

    try {
        BlockThrow();   // terminate called after throwing an instance of 'int'
    } catch(...) {
        cout << "Found throw 1." << endl;
    }
}
  • noexcept 作为一个操作符,通常可以用于模板
template 
void fun() noexcept(noexcept(T())) {

}

fun() 是否是一个 noexcept 函数,由 T() 表达式是否抛出异常决定。这里,第二个 noexcept 就是一个 noexcept 操作符。当其参数是一个有可能抛出异常的表达式的时候,其返回值为 false,反之为 ture. 这样一来,我们就可以使模板函数根据条件实现 noexcept 修饰的版本或无 noexcept 修饰的版本。从泛型编程的角度来看,这样的设计保证了关于“函数是否抛出异常”这样的问题可以通过表达式进行推导。因此这也可以视作 C++11 为了更好地支持泛型编程而引入的特性。

虽然 noexcept 修饰的函数通过 std::terminate 的调用来结束程序的执行的方式可能会带来很多问题,比如无法保证对象的析构函数的正常调用,无法保证栈空间的释放等,但很多时候,“暴力”地终止整个程序确实是很简单有效的做法。事实上,noexcept 被广泛地,系统地应用在 C++11 的标准库中,用于提高标准库的性能,以及满足一些阻止异常扩散的需求。

// C++98
template class A {
public:
    static constexpr T min() throw() { return T(); }
    static constexpr T max() throw() { return T(); }
    static constexpr T lowest() throw() { return T(); }
};

// C++11
template class A {
public:
    static constexpr T min() noexcept { return T(); }
    static constexpr T max() noexcept { return T(); }
    static constexpr T lowest() noexcept { return T(); }
}
// C++98
void* operator new(std::size_t) throw(std::bad_alloc);
void* operator new[](std::size_t) throw(std::bad_alloc);

// C++11
void* operator new(std::size_t) noexcept(false);
void* operator new[](std::size_t) noexcept(false);

C++11 默认将 delete 函数设置成 noexcept,可以保证应用程序的安全性。

void operator delete(void *) noexcept;
void operator delete[] (void *) noexcept;

C++11 默认让类的析构函数默认也是 noexcept(ture) 的。如果程序员显式的为析构函数制定了 noexcept ,或者让类的基类成员有 noexcept(false) 的西沟函数,析构函数就不会再保持默认值。

#include 
using namespace std;

struct A {
    ~A() { throw 1; }
};

struct B {
    ~B() noexcept(false) { throw 2; }
};

struct C {
    B b;
};

int funA() { A a; }
int funB() { B b; }
int funC() { C c; }


int main() {
    try {
        funB();
    } catch(...) {
        cout << "caught funB." << endl; // caught funB.
    }

    try {
        funC();
    } catch(...) {
        cout << "caught funC." << endl; // caught funC.
    }

    try {
        funA(); // terminate called after throwing an instance of 'int'
    } catch(...){
        cout << "caught funA." << endl;
    }
}

快速初始化成员变量

  • C++98:
    • 使用 ‘=’ 初始化类中成员变量,成员变量必须满足:
      ① static ② const ③ 整型或枚举型
class Init {
public:
    Init() : a(0) {}
    Init(int d) : a(d) {}

private:
    int a;
    const static int b = 0;  // ok
    int c = 1;               // error
    static int d = 0;        // error
    static const double e = 1.3;      // error,不是整型或枚举型
    static const char *const f = "e"; // error, 不是整型或枚举型
}
  • C++11
    • 允许非静态成员变量的初始化,且有多种形式。
struct {
    int a = 1;         // 使用 '=' 初始化
    double e {2.3};    // 使用 '{}' 初始化
};
#include 

using namespace std;

struct C {
    C(int i) :
        c(i) {}

    int c;
};

struct Init {
    int a = 1;
    string b("Hello");  // error
    C c(1);             // error
}

圆括号表达式初始化非静态成员 b 和 c 都会出错。

  • C++11 支持就地初始化非静态成员的同时,又支持初始化列表。如果两者同时使用,是否会冲突?
#include 
using namespace std;

struct Mem {
    Mem() { cout << "Mem defulat, num = " << num << endl; }
    Mem(int i)
        : num(i) {
            cout << "Mem defulat, num = " << num << endl;
        }

    int num = 2; // 使用 = 初始化非静态成员
};

class Group {
public:
    Group() { cout << "Group default. val: " << val << endl; }
    Group(int i)
        : val('G'),
          a(i) {
              cout <<"Group. val: " << val << endl;
          }
    void NumOfA() { cout << "number of A: " << a.num << endl; }
    void NumOfB() { cout << "number of B: " << b.num << endl; }

private:
    char val{'g'}; // 使用 {} 初始化非静态成员
    Mem a;
    Mem b{19};     // 使用 {} 初始化非静态成员
};

int main() {
    Mem member;  // Mem defulat, num = 2
    Group group; // Mem default, num = 2
                 // Mem default, num = 19
                 // Group default. val: g

    group.NumOfA();  // number of A: 2
    group.NumOfB();  // number of B: 19

    Group group2(7); // Mem defulat, num = 7
                     // Mem defulat, num = 19
                     // Group. val: G

    group2.NumOfA();  // number of A: 7
    group2.NumOfB();  // number of B: 19
}

对于非常量静态成员变量,C++11与C++98保持了一致,程序员需要到头文件以外去定义它,这会保证编译时,类静态成员的定义最后只会存在于一个目标文件中。

非静态成员的 sizeof

  • C++98
    • 无法对非静态成员变量使用sizeof
#include 
using namespace std;

struct People {
public:
    int hand;
    static People *all;
}

int main()
{
    Pople p;
    cout << sizeof(p.hand) << endl;       // C++98 ok, C++11 ok
    cout << sizeof(Pople::all) << endl;   // C++98 ok, C++11 ok
    cout << sizeof(People::hand) << endl; // C++98 err, C++11 ok
}

扩展的 friend 语法

friend 关键字用于声明类的 友元, 友元可以无视类中的成员属性。无论是public、protected或private,友元类或友元函数都可以访问,这完全破坏了面向对象中封装性的概念。通常,专家建议使用 Get/Set 方法访问类成员,但是,friend会使程序员少些很多代码。

class Poly;
typedef Poly P;

class LiLei {
    friend class Poly; // C++98 ok, C++11 ok
};
class Jim {
    friend Poly;       // C++98 error, C++11 ok
};
class HanMeiMei {
    friend P;          // C++98 error, C++11 ok
}

程序员可以为类模板声明友元

class P;
template 
class People {
    friend T;
};

People

pp; // 类型 P 在这里是 People 类型的友元 People Pi; // 对于 int 类型模板参数,友元声明被忽略

// 为了方便测试,进行了危险的定义
#ifdef UNIT_TEST
#define private public
#endif
class Defender {
public:
    void Defender(int x, int y) {}
    void Trackle(int x, int y) {}

private:
    int pos_x = 15;
    int pos_y = 0;
    int speed = 2;
    int stamina = 120;
};

class Attacker {
public:
    void Move(int x, int y) {}
    void SpeedUp(float ration) {}

private:
    int pos_x = 0;
    int pos_y = -30;
    int speed = 3;
    int stamina = 100;
};

#ifdef UNIT_TEST
class Validator {
public:
    void Validate(int x, int y, Defender & d) { }
    void Validate(int x, int y, Attacker & a) { }
};

int main() {
    Defender d;
    Attacker a;
    a.Move(15, 30);
    d.Defence(15, 30);
    a.SpeedUp(1.5f);
    d.Defence(15, 30);
    Validator v;
    v.Validate(7, 0, d);
    v.Validate(1, -10, a);
    return 0;
}
#endif

将 private 关键字统一替换成了 public 关键字。
这是危险的,在C++11中进行改良:

template 
class DefenderT {
public:
    friend T;
    void Defence(int x, int y) {}
    void Trackle(int x, int y) {}

private:
    int pos_x = 15;
    int pos_y = 0;
    int speed = 2;
    int stamina = 120;
};

template 
class AttackerT {
public:
    friend T;
    void Move(int x, int y) {}
    void SpeedUp(float ration) {}

private:
    int pos_x = 0;
    int pos_y = -30;
    int speed = 3;
    int stamina = 100;
};

using Defender = DefenderT;  // 普通的类定义,使用 int 做参数
using Attacker = AttackerT;

class Validator {
public:
    void Validate(int x, int y, Defender & d) { }
    void Validate(int x, int y, Attacker & a) { }
};

using DefenderTest = DefenderT;  // 测试专用的定义,Validator 类成为友元
using AttackerTest = AttackerT;

int main() {
    Defender d;
    Attacker a;
    a.Move(15, 30);
    d.Defence(15, 30);
    a.SpeedUp(1.5f);
    d.Defence(15, 30);
    Validator v;
    v.Validate(7, 0, d);
    v.Validate(1, -10, a);
    return 0;
}

final/override 控制

  • 重载(overload):是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
  • 重写(override):派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

virual 关键字

#include 

class A {
public:
    void print() {
        std::cout << "A" << std::endl;
    }
};

class B : public A {
public:
    void print() {
        std::cout << "B" << std::endl;
    }
};

int main()
{
    A *b = new B();

    b->print();  // A

    return 0;
}

将 print() 加上 virual 关键字修饰

#include 

class A {
public:
    virtual void print() {
        std::cout << "A" << std::endl;
    }
};

class B : public A {
public:
    void print() {
        std::cout << "B" << std::endl;
    }
};

int main()
{
    A *b = new B();

    b->print();  // B

    return 0;
}

说明,C++ 中多态函数必须使用 virtual 关键字进行修饰。

#include 
using namespace std;

class MathObject {
public:
    virtual double Arith() = 0;
    virtual void Prin() = 0;
};

class Printable : public MathObject {
public:
    double Arith() = 0;
    void Print() {  // C++98中我们无法阻止该接口被重写
        cout << "Output is : " << Arith() << endl;
    }
};

class Add2 : public Printable {
public:
    Add2(double a, double b)
        : x(a),
          y(b) { }

    double Arith() {
        return x + y;
    }

private:
    double x, y;
};

class Mul3 : public Printable {
public:
    Mul3(double a, double b, double c)
        : x (a),
          y (b),
          z (c) {}

    double Arith() {
        return x * y * z;
    }
private:
    double x, y, z;
};

本来程序希望的结果是 Printable 重写 Print() 方法,其他类不再重写该方法,而达到打印方式一致的目的。但是C++98中无法阻止 Print() 被重写。

C++11 和 Java 类似,通过 final 关键字阻止 函数继续重写。

struct Object{
    virtual void fun() = 0;
};

struct Base : public Object {
    void fun() final;   // 声明为final
};

struct Derived : public Base {
    void fun();     // 无法通过编译
};

在 C++ 中,final 关键字同样可以修饰虚函数,但是虚函数的目的就是被重写,如果用 final 修饰,则失去了 虚函数 的意义。

基类声明为 virtual 的函数,之后的重写不再需要声明函数为 virtual,即使派生类声明为 virtual,该关键字也是编译器可以忽略的。这导致了阅读上的困难。程序员无法确定一个函数是虚函数还是非虚函数。

C++11 为了帮助程序员写继承结构复杂的类,引入了虚函数描述符 override,如果派生类在虚函数声明时使用了 override 描述符,那么该函数必须重写其基类的同名函数,否则无法编译通过。

struct Base {
    virtual void Turing() = 0;
    virtual void Dijkstra() = 0;
    virtual void VNeumann(int g) = 0;

    virtual void DKnuth() const;
    void Print();
};

struct DerivedMid: public Base {
    // void VNeumann(double g);
    // 接口被隔离了,曾想多一个版本的VNeumann函数
};

struct DerivedTop : public DerivedMid {
    void Turing() override;
    void Dikjstra() override;           // 无法通过编译,拼写错误,并非重写
    void VNeumann(double g) override;   // 无法通过编译,参数不一致,并非重写
    void DKnuth() override;             // 无法通过编译,常量性不一致,并非重写
    void Print() override;              // 无法通过编译,非虚函数重写
};

如果没有 override 的修饰,程序员可能没有意识到自己犯了这么多错误。

模板函数的默认模板参数

#include 
using namespace std;

// 定义一个函数模板
template 
void TempFun(T a) {
    cout << a << endl;
}

int main() {
    TempFun(1);     // 1, (实例化为TempFun(1))
    TempFun("1");   // 1, (实例化为TempFun("1"))
}

C++11 支持默认模板参数

void DefParm(int m = 3) {}  // c++98 - 编译通过,c++11 - 编译通过
template 
    class DefClass {};      // c++98 - 编译通过,c++11 - 编译通过
template 
    void DefTempParm() {};  // c++98 - 编译失败,c++11 - 编译通过
template class DefClass1;
template class DefClass2;   // 无法通过编译

template class DefClass3;
template class DefClass4;            // 无法通过编译

/*
 * 类模板必须遵守 从右往左 的顺序
*/

template void DefFunc1(T1 a, T2 b);
template void DefFunc2(T a);
/*
 * 函数模板则没有要求,随意
*/

函数模板默认参数的推导由实参推导

template 
void f(T t = 0, U u = 0); // 同时使用了默认模板参数和默认模板函数参数

void g() {
    f(1, 'c');      // f(1,'c')
    f(1);           // f(1,0), 使用了默认模板参数double
    f();            // 错误: T无法被推导出来
    f();       // f(0,0), 使用了默认模板参数double
    f();  // f(0,0)
}

强调一点:模板函数的默认形参不是模板参数推导的依据。

外部模板

为什么需要外部模板

C 中 extern 的目的:

extern int i;

一个文件定义 i, 多个文件声明 i, 但是 i 只有一份数据。

对函数模板来说,存在一模一样的问题。不同的是,发生问题的不是变量,而是函数。

// 在 test.h 声明一个模板函数
template 
void fun(T) {}
//--------------------------------------------------------------

// test1.cpp
#include "test.h"
void test1() { fun(3); }
//--------------------------------------------------------------

// test2.cpp
#include "test.h"
void test2() { fun(4); }
//--------------------------------------------------------------
/*
 * 问题:
 * 由于两个源代码使用的模板函数的参数类型一致,所以再编译 test1.cpp 时编译器会实例化 fun(int).
 * 在编译 test2.cpp 时,编译器会再一次实例化函数 fun(int)。
 * 那么结果就是 test1.o 和 test2.o 会有两份一模一样的函数 fun(int) 代码.
*/

数据重复,编译器往往无法分辨是否要共享数据。

代码重复,为了节省空间,保留其中之一就可以了。事实上,大部分链接器也是这么做的。链接器通过一些编译器辅助的手段将重复的模板函数代码 fun(int) 删除掉,只保留了单个副本。

问题是:对于源码中的每一处模板实例化,编译器都需要去做实例化的工作;而在链接时,链接器还需要删除重复的实例化代码。很明显,这太麻烦。

显式的实例化与外部模板的声明

显示实例化(Explicit Instantiation)

template 
void fun(T) {
}

// 对 fun 模板显式实例化
template (int);
/*
 * 编译器在本编译单元中实例化出一个 fun(int) 函数(强制实例化)。
*/

// C++11 又加入了 外部模板(Extern Template)
extern template void fun(int); // 外部模板声明

我们重新修改下上述代码:

// test1.cpp
template void fun(int); // 显式实例化
void test1() {
    fun(3);
}

//----------------------------------------------------------------

// test2.cpp
extern template void fun(int);  // 外部模板声明
void test() {
    fun(3);
}

注意问题:如果外部模板声明出现在某个编译单元中,那么与之对应的显式实例化必须出现于另一个编译单元中或同一个编译单元的后续代码中。

外部模板声明不能用于一个静态函数(文件域函数),但是可以用于类静态成员函数(因为静态函数没有外部链接属性,不能再本编译单元外出现)。

局部和匿名类型做模板实参

  • C++98:
    • 局部类型和匿名类型在C++98中不能做模板的实参
template 
class X {};

template 
void TempFun(T t){};

struct A{} a;
struct {int i;} b;           // b是匿名类型变量
typedef struct {int i;} B;   // B是匿名类型

void Fun()
{
    struct C {} c;          // C是局部类型

    X x1;    // C++98 - 通过,C++11 - 通过
    X x2;    // C++98 - 错误,C++11 - 通过
    X x3;    // C++98 - 错误,C++11 - 通过
    TempFun(a); // C++98 - 通过,C++11 - 通过
    TempFun(b); // C++98 - 错误,C++11 - 通过
    TempFun(c); // C++98 - 错误,C++11 - 通过
}

虽然匿名类型可以被模板参数接受了,但以下写法不被接受。

template 
struct MyTemplate { };

int main() { 
    // MyTemplate t; // 无法编译通过, 匿名类型的声明不能在模板实参位置
    MyTemplate t; // 无法编译通过, 匿名类型的声明不能在模板实参位置
    return 0;
}

你可能感兴趣的:(兼容性和稳定性)