深度阅读:大学生课外知识补充,这些课堂上不教的 C++ 的基本特性你都知道吗?

来源:知乎 王师傅的专栏ios

 

C++ 做为一个历史久远,功能丰(yong)富(zhong)并且标准与时俱进的语言,理应什么都能作,什么都用得起来。不过平常使用中咱们初学者真的好像只学到了其中的一部分,对于一些另类的特性都不怎了解。这篇文章列举一些 C++ 的用到的或多或少,可是学习中几乎都会忽视的语言特(lou)性(dong),但愿读者看完能有收获。若是你没有收获,建议去作一名语言律师~c++

如下一部分都是从 SO 的高票问题找出的,还有一些是业余的收集,若是有纰漏敬请指出。固然过于高(zhuang)级(bi)的奇技淫巧就没有必要介绍了。如下例子纯手打,在 clang 测试过。golang

交换数组变量和下标 (c)

若是你想获取数组元素,能够写 A[i] 或者是 i[A]json

int a[] { 1,2,3 };
cout << a[1] << endl; // 2
cout << 1[a] << endl; // 2

由于数组取下标至关于计算指针地址与偏移量,而 *(A+i) 和 *(i+A) 意思是相同的。设计模式

合并字符串 (c)

const char *s =
   "welcome to my\n"
   "    home!\n"
   " enjoy life!\n";

最后 s 的值是 "welcome to my\n home!\n enjoy life!\n",即以上三行合起来。这个语法源于 C,在 C 标准库宏中普遍出现。大多数常见语言也都有这个特性。数组

逻辑运算关键字 (c++98)

对于布尔值运算,C++ 也提供 andor 这样的关键字,与 &&|| 等做用相同:app

bool b = not (false or true and false);
int i = 8 xor 18;
cout << b << endl; // 1
cout << i << endl; // 26

双字符组 Digraph / 三字符组 Trigraph (c)

过往部分地区的人们的键盘不方便打出大括弧之类的特殊符号,因此相似字符串的转义字符,这些语法符号也能够被另外常见的符号所表明ide

%:include <iostream>
using namespace std;
int main() <%
   int a<::> = <% 1,2,3 %>;
   cout << a<:1:> << endl; // 2
   // trigraph 写法,必须开启 -trigraphs 选项
   cout << a??(1??) << endl; // 2
%>

是能够在现代编译器上编译的。尽管再也不有用,Digraph 仍然存在,不过它的兄弟 Trigraph 则很早就已经被废弃了。函数

变量类型修饰符的顺序 (c)

咱们知道像 conststatic 等变量修饰符能够随意交换顺序,不过你有没有想过这种状况呢?工具

long const int static long value = 2;

它能够经过编译,并且类型是 const long long

uniform/aggregate initialization (c++11)

C++98 中对象和内建类型的初始化方式五花八门,为了解决这个问题,C++11 引入了所谓的集合初始化:

int a {};     // 默认初始化,对于整形,初始化为 0,至关于 bzero
int b {10};   // 初始化为 10
int f = {}, g = {50}; // !
double d {10};
const char *s {"hello"};
cout << f << ' '<< g << endl; // 0 50

这旨在使内建非对象的类型(基础类型和数组)都能像用户定义的对象同样以相同的语法被初始化。这样就至关于“像 int 这类的类型也有了 initializer_list 构造函数”,这给模板函数带来了很大的方便:

struct int_wrapper {
   int value;
   int_wrapper(int value_): value{value_} {} // <-
};

template<class T> T factory() {
   return T{114514};  // <-
}

int main() {
   int i = factory<int>();
   int_wrapper w = factory<int_wrapper>();
}

本文的第一个例子就用到了这个特性。统一和集合初始化的门道不少,建议你们查阅相关专业资料。

int z(10.1); // OK. 强转为 10
// int x{10.1}; // 报错

函数返回 void (c)

若是一个函数的返回类型是 void,那么你是能够用 return 返回它的:

void print_nums(int i) {
   if (i == 0)
       return;
   else {
       cout << i << endl;
       return print_nums(i - 1); // <- return
   }
}

这个特性我以为你们应该都会知道,并且它在动态语言里也很常见。和上一条同样,其在模板中有应用。可是这个例子能够写得更激进:

void print_nums(int i) {
   return i == 0 ? void() : (cout << i << endl, print_nums(i - 1));
}

表达式返回左值 (c++98)

像函数或者表达式返回一个左值 (lvalue) 是 C++ 中最基础的操做,不过有时候也能玩出花儿来:

int divisibleby3 = 0, others = 0;
for (int i = 0; i < 1000; ++i)
   (i % 3 == 0 ? divisibleby3 : others) += 1;
cout << divisibleby3 << ' ' << others << endl; // 334 666

只要等号左边是左值,什么东西均可以放。哪怕你是有逗号运算符,仍是 lambda。

int total = 0;
("loudly get the total count",
   ([&]() -> int& {
       cout << "assigning total count!\n";
       return total;
   })())
   = divisibleby3 + others; // total = 1000

小心被同事打死。

整形字面量分隔符 (c++14)

C++14 不只加入了二进制字面量的支持 (0bxxxyyyzzz)还加入了分隔符,不再用担忧数字太多眼睛看花的问题了:

int opcode = 0b0001'0011'1010;
double e = 2.7'1828'1828'459045;

函数域 try/catch (c++98)

不多有人知道函数,构造函数,析构函数能够声明全局的异常捕获,就像这样:

int bad_func() { throw runtime_error("hahaha I crashed"); return 0; }

struct try_struct {
   int x, y;
   try_struct() try: x{0}, y{bad_func()} {}
   catch (...) {
       cerr << "it is crashing! I can't stop it." << endl;
   }
};

int main() try {
   try_struct t;
} catch (exception &e) {
   cerr << "program crashed. reason: " << e.what() << endl;
   return 1;
}
// 输出:
// it is crashing! I can't stop.
// program crashed. reason: hahaha I crashed

对于函数,在这里其做用就至关于在 main 下多写一层花括号。构造函数相似,可是在 catch块中若是用户不抛出异常,编译器会必定隐式抛出原异常。(en.cppreference.com/w/c)

匿名类 (c++98)

C 就支持匿名 (untagged) 类的定义,不过 C++ 更进一步,你能够在函数的任何地方定义匿名类,甚至循环变量的声明中,如如下反转数组的函数:

void reverse(int *arr, int size) {
   for (struct { int l, r; } i = { 0, size-1 }; i.l <= i.r; ++i.l, --i.r) {
       swap(arr[i.l], arr[i.r]);
   }
}

匿名类也能够出如今 using 后(自行尝试)。有了 C++ 的 auto (c++14),用户甚至能够返回真~匿名类:

auto divide(int x, int y) {
   struct  /* unnamed */ {
       int q, r;
       // cannot define friend function
       ostream &print(ostream &os) const {
           os << "quotient: " << q << " remainder: " << r;
           return os;
       }
   } s { x / y, x % y };
   return s;
}

int main() {
   divide(11,2).print(cout) << endl;
}
// 输出
// quotient: 5 remainder: 1

除此还能够定义虚函数和构造析构函数。若是仔细想一想,其原理和 lambda 差很少。

if 语句中声明变量 (c++98)

以下所示:

bool get_result() { return false; }

int main() {
   if (bool i = get_result())
       cout << "success!" << endl;
   else
       cout << "fail" << endl;
   // i 不可用
}
// 输出
// fail

在 while 语句中也可使用这个结构。例子中 i 的生命周期限于这个 if/else 块中。其就至关于

int main() {
   {
       bool i = get_result();
       if (i)
           cout << "success!" << endl;
       else
           cout << "fail" << endl;
   }
}

能在做用域中声明变量,这和 C 程序中的相似技巧很不一样。

在 C++17 中,这个特性被增强了。不只能够声明,还能够像 for 循环同样附加条件,和 golang 很像

int get_result() { return 17; }

int main() {
   if (int i = get_result(); i >= 18)
       cout << "ok." << endl;
   else
       cout << "fail. your age is too low: " << i << endl;
}
// 输出
// fail. your age is too low: 17

结构化绑定 (c++17)

C++17 的新语法使得咱们能够在一个语句里解包变量,例如解包一个长度为 3 的数组:

int arr[3] { 1,2,9 };
// 至关于把数组的值都拷贝到这三个变量里
auto [cpy0, cpy1, cpy2] = arr;
cpy0 = 2;
cout << cpy0 << ' ' << arr[0] << endl; // 2 1
// 至关于把数组的值起了三个别名
auto &[ref0, ref1, ref2] = arr;
ref0 = 2;
cout << ref0 << ' ' << arr[0] << endl; // 2 2

这个特性能够解包标准库中的 std::tuple,也能够以成员声明顺序解包一个结构体:

auto get_data() {
   struct { int code; string header, body; }
       r { 200, "200 OK\r\nContent-Type: application/json", "{}" };
   return r;
}

int main() {
   // std::ignore 用来丢弃一个不须要的数据
   if (auto [code, ignore, json] = get_data(); code == 200)
       cout << "success: " << json << endl;
   else
       throw runtime_error{"request failed"};
}

在 C++17 以前,使用 std::tie 也能够实现相相似的效果:

int a = 1, b = 2, c = 3;
// std::make_tuple 封包,std::tie 解包
tie(a, b, c) = make_tuple(b, c, a);
cout << a << b << c << endl; // 231

placement new (c++98)

当对象被 new 的时候,你们都知道发生的过程是先调用 operator new 来分配内存,再调用构造函数。不过 new 拥有另外一个重载,咱们能够跳过度配内存的一步,也就是在咱们指定的内存区域直接初始化对象。

struct vector2d {
   string name;
   long x, y;
   vector2d(long x_, long y_): x(x_), y(y_), name("unnamed") {}
};

int main() {
   char buff[1 << 10];
   // 在 buffer 上建立对象
   vector2d *p = new (buff) vector2d(3, 4);

   cout << p->name << "("
        << p->x << ", " << p->y << ")" << endl;
   cout << *reinterpret_cast<string *>(buff)
        << "("
        << *reinterpret_cast<long *>(buff + sizeof(string))
        << ", "
        << *reinterpret_cast<long *>(buff + sizeof(string) + sizeof(long))
        << ")" << endl;

   // 析构对象
   p->~vector2d();
   // 不保证原内存必定会被清零!
}
// 输出
// unnamed(3, 4)
// unnamed(3, 4)

全局命名空间操做符 (c++98)

可使用 :: 来显式表示当前所表示的符号来自于全局命名空间,从而消除歧义:

namespace un {
   void func() { cout << "from namespace un\n"; }
}

void func() { cout << "from global\n"; }

using namespace un;

int main() {
   ::func();
}
// 输出
// from global

匿名命名空间 (c++98)

使用匿名的命名空间能够限制符号的可见性。当你写下这段代码时:

// test.cpp
namespace {
   void local_function() {}
}

它至关于这段代码:

// test.cpp
namespace ___some_unique_name_test_cpp_XADDdadh876Sxb {}
using namespace ___some_unique_name_test_cpp_XADDdadh876Sxb;
namespace ___some_unique_name_test_cpp_XADDdadh876Sxb {
   void local_function() {}
}

也就是一个独一无二,对于当前编译单元的命名空间被建立。由于这个命名空间只在这个文件中引用,故而用户只能够在当前文件 (test.cpp) 中引用 local_function。这样作就避免了不一样文件中可能出现的名称冲突。其效果与

// test.cpp
static void local_function() {}

相同。对于匿名命名空间和 static,能够参考这个 SO 问题: stackoverflow.com/quest

“内联” (inline) 命名空间 (c++11)

若是不是库做者,可能会对这个特性十分惊讶,内联这个关键字的含义已经和 static 同样要起飞了。若是一个命名空间被内联,那么它会被给予优先级。

namespace un {
   inline namespace v2 {
       void func() { cout << "good func.\n"; }
   }
   namespace v1 {
       void func() { cout << "old func. use v2 instead.\n"; }
   }
}

int main() {
   un::func();
   un::v2::func();
   un::v1::func();
}
// 输出
// good func.
// good func.
// old func. use v2 instead.

自定义字面量 (c++11)

自从 C++11,用户能够本身重载 operator"" 来以字面量的形式初始化对象:

long double constexpr operator""_deg (long double deg) {
   return deg * 3.14159265358979323846264L / 180;
}

int main() {
   long double r = 270.0_deg; // r = 4.71239...
}

重载 , 运算符 (c++98)

没想到吧,逗号也能重载!这个例子受 boost/assign/std/vector.hpp 的启发,能够简便地向标准库的 vector 中一个个地插入元素:

template<class VT>
struct vector_inserter {
   VT &vec;
   vector_inserter(VT &vec_): vec(vec_) {}
   template<class TT>
   vector_inserter<VT> &operator,(TT &&v) {
       vec.emplace_back(forward<TT>(v));
       return *this;
   }
};

template<class T, class Alloc, class TT>
vector_inserter<vector<T, Alloc>>
operator+=(vector<T, Alloc> &vec, TT &&v) {
   vec.emplace_back(forward<TT>(v));
   return {vec};
}

int main() {
   vector<int> v;
   // 使用 emplace_back 赋值
   v += 1,2,3,4,5,6,7;
   cout << v.size() << endl; // 7
}

逗号操做符还有一处另类的地方是当操做数的类型为 void 时,没有重载能够改变它的行为。这能够(在c++98)用来检测一个表达式是否为 void: stackoverflow.com/quest

除此以外大多数重载 , 的行为都不是什么好行为,知道能重载就好,想用的话最好三思!

类成员声明顺序 (c++98)

类成员在使用时不须要关心它们的声明顺序,因此咱们能够先使用变量再定义。因此有时为了方便彻底能够把全部的代码写在一个类里。

struct Program {
   vector<string> args;
   void Main() {
       cout << args[0] << ": " << a + foo() << endl;
   }
   int a = 3;
   int foo() { return 7; }
};

int main(int argc, char **argv) {
   Program{vector<string>(argv, argv+argc)}.Main();
}

类函数引用修饰符 (c++11)

不一样于 constnoexcept 这样仅修饰类方法自己行为的修饰符,& 和 && 修饰符会根据 *this 是左值仍是右值引用来选择合适的重载。

struct echoer {
   void echo() const & {
       cout << "I have long live!\n";
   }
   void echo() const && {
       cout << "I am dying!\n";
   }
};

int main() {
   echoer e;
   e.echo();
   echoer().echo();
}
// 输出
// I have long live!
// I am dying!

类命名空间操做符 (c++98)

只有子类型的指针,如何调用父类型的方法?用命名空间。

struct Father {
   virtual void say() {
       cout << "hello from father\n";
   }
};
struct Mother {
   virtual void say() {
       cout << "hello from mother\n";
   }
};
struct Derived1: Father, Mother {
   void say() {
       Father::say(); Mother::say();
       cout << "hello from derived1\n";
   }
};

int main() {
   Derived1 *p = new Derived1{};
   p->say();
   p->Father::say();
}
// 输出
// hello from father
// hello from mother
// hello from derived1
// hello from father

构造器委托 (c++11)

可使用和继承类相同的语法在一个构造函数中调用另一个构造函数:

struct CitizenRecord {
   string first, middle, last;
   CitizenRecord(string first_, string middle_, string last_)
       : first{move(first_)}, middle{move(middle_)}, last{move(last_)} {
       if (first == "chao") cout << "important person inited\n";
   }
   // 默认参数委托
   CitizenRecord(string first_, string last_)
       : CitizenRecord{move(first_), "", move(last_)} {}
   // 拷贝构造函数委托
   CitizenRecord(const CitizenRecord &o)
       : CitizenRecord{o.first, o.middle, o.last} {}
   // 移动构造函数委托
   CitizenRecord(CitizenRecord &&o)
       : CitizenRecord{move(o.first), move(o.middle), move(o.last)} {}
};

这个特性有时能够适量减小代码重复,或者转发默认参数。

自定义枚举存储类型 (c++11)

C 时代的枚举中定义中的整形都必须是 int,在 C++ 中能够自定义这些类型:

enum class sizes : size_t {
   ZERO    = 0,
   LITTLE  = 1ULL << 10,
   MEDIUM  = 1ULL << 20,
   MANY    = 1ULL << 30,
   JUMBO   = 1ULL << 40,
};

int main() {
   cout << sizeof sizes::JUMBO << endl; // 8
}

其中 sizes::ZERO 这些常量都持有 size_t 类型。顺便一提 enum 和 enum class 的区别是前者的做用域是全局,后者则须要加上 sizes::.

模板联合 (c++98)

联合也是 C++ 的类,也支持方法,构造和析构,一样还有模板参数。

template<class T, class U>
union one_of {
private:
   T left_value;
   U right_value;
public:
   T &left_cast() { return left_value; }
   U &right_cast() { return right_value; }
};

int main() {
   one_of<long double, long long> u;
   u.left_cast() = 3.3;
   cout << u.left_cast() << endl;   // 3.3
   u.right_cast() = 1LL << 32;
   cout << u.right_cast() << endl;  // 4294967296
}

一点须要注意的是,union 的成员必须是 "trivial" 的,也就是无需显式构造函数,不然轻则编译失败,重则 UB (未定义行为)。

模板位域 (c++98),模板 align (c++11)

你可能很熟悉 C/C++ 的位域特性,可是你知道位域能够写进模板吗?

template<size_t I, size_t J>
struct some_bits {
   int32_t a : I, b : I;
   int32_t c : J;
};

int main() {
   some_bits<8, 16> s;
   s.a = 127;
   s.b = 128;
   s.c = 65535;
   cout << "a: " << s.a << "\nb: " << s.b << "\nc: " << s.c
        << "\ntotal size: " << sizeof s << endl;
}
// 输出
// a: 127
// b: -128
// c: -1
// total size: 4

与此类似,C++11 的 alignas 也能够参与模板:

template<size_t Size>
struct alignas(Size) empty_space {};

int main() {
   empty_space<64> pad;
   cout << sizeof pad << endl; // 64
}

类成员指针 (c++98)

正如 int(*)(int, int) 表明一个接受两个整形返回一个整形的函数指针,对于一个类型 Tint(T::*)(int, int) 表示一个非静态的类成员函数的类型。这是由于这种函数总会有一个隐式的 this 指针做为第一个参数,因此咱们须要使用不一样的语法来区分它们。

struct echoer {
   string name;
   void echo1(string &c) const {
       cout << "I'm " << name << ". hello " << c << "!\n";
   }
   static void echo2(string &c) {
       cout << c << "! I have long/short live!\n";
   }
};

int main() {
   echoer me {"mia"};
   string you {"alice"};
   string echoer::*aptr = &echoer::name; // 类成员变量指针
   void (echoer::*mptr)(string&) const = &echoer::echo1; // 类方法指针
   void (*smptr)(string&) = &echoer::echo2; // 类静态方法指针 (就是普通函数指针)
   void (&smref)(string&) = echoer::echo2; // 类静态方法引用

   (me.*mptr)(you);
   smptr(you);
   smref(you);

   echoer *another = new echoer{"valencia"};
   (another->*mptr)(me.*aptr);
   delete another;
}
// 输出
// I'm mia. hello alice!
// alice! I have long/short live!
// alice! I have long/short live!
// I'm valencia. hello mia!

成员指针和函数指针是实现 traits 必不可少的。

返回值后置 (c++11)

R (Args) 等同于 auto (Args) -> R。返回值后置能够用在不少地方,如常见的 lambda:

const auto add = [](int a, int b) -> int { // 返回类型为 int
   return a + b;
};

以及在通常的函数中表示返回类型依赖于参数:

template<class T>
auto serialize_object(T &obj) -> decltype(obj.serialize()) {
// 返回类型为 obj.serialize() 的类型
   return obj.serialize();
}

通常鲜为人知的是,后置返回值还能够简化函数指针的写法。

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
auto produce(int op) -> auto (*)(int, int) -> int { // 声明后置
   if (op == 0) return &add;
   else return &mul;
}
// 不后置的话这个函数的声明是这样的:
// int (*(produce(int op)))(int, int);

int main() {
   using producer_t = auto (*)(int) -> auto (*)(int, int) -> int; // 指针后置
   // 不后置的话这个类型的声明是这样的
   // using producer_t = int (*((*)(int)))(int, int);
   producer_t ptr {&produce};
   cout << ptr(0)(1, 2) << endl; // 3
}

类成员函数也相似地能够这样写。在 C++14 中函数声明的返回值能够只写 auto 来让编译器自动推导返回类型了。

由于函数声明 = 后置返回值的函数声明,咱们能够在模板参数里也使用这个语法,例如 std::function:

const function<auto (int, int) -> int> factorial
   = [&](int n, int acc) {
   return n == 1 ? acc : factorial(n-1, acc * n);
};
cout << factorial(5, 1) << endl; // 120

不求值表达式和编译期常量 (c++98)

在 C 时代就有仅存在于编译期的表达式。在程序编译运行后,这些代码就被删除,替换成常量了。好比常见的 sizeof 运算符

int count = 0;
cout << sizeof(count++) << endl; // 4
cout << count << endl;           // 0

由于 sizeof(count++) 直接在编译期被替换成了 4,其所本来应该具备的反作用也会没有做用。
在 C++11 中引入了 decltype,它和 noexcept, sizeof 操做符相同,仅存在于编译时期。而这个操做符的做用是得到一个表达式的类型,譬如如此:

decltype(new int[10]) ptr {};        // 等同于 int *ptr = nullptr; 根本没有内存被分配!
decltype(int{} * double{}) value {}; // 只是声明一个变量,这个变量的类型是 int 乘 double 的类型
                                    // 具体是什么类型我本身不知道

与其一同来临的还有 declval 等。同时还有 constexpr 表达式,能够保证表达式必定会在编译期内计算完毕,不拖延到运行时。

int constexpr fib(int n) {
   return n == 0 ? 0 : n == 1 ? 1 : fib(n-1) + fib(n-2);
}
int main() {
   int constexpr x = fib(20); // 彻底等同于 x = 6765!运行程序时根本不会再去计算
}

实际上于此咱们能够解释凭什么有的函数没有定义就能跑:

template<int v> struct i32 { static constexpr int value = v; };
template<int v> i32<v * 2 + 1> weird_func(i32<v>);
i32<0> weird_func(...);

int main() {
   cout << decltype(weird_func(i32<7>{}))::value << endl; // 15
   cout << decltype(weird_func(7))::value << endl;        // 0
}

由于它们根本没跑。

decltype(auto) (c++14)

以前讲过 auto 能够做为函数的返回值来自动推导,不过对于 auto 和 decltype(auto),虽然大多数状况下后者是累赘,也存在二者意义不一样状况:

auto incr1(int &i) { ++i; return i; } // 返回: int (拷贝)
decltype(auto) incr2(int &i) { ++i; return i; } // 返回: int&

int main() {
   int a = 0, b = 0;
   // cout << incr1(incr1(a)) << endl;  // 报错
   cout << incr2(incr2(b)) << endl;     // 输出 2
}

auto 会看所要推导的变量原生类型 T,而 decltype(auto) 会推导出变量的实际类型 T& 或是 T&&

引用折叠和万能引用 (universal reference) (c++11)

T&& 不必定是 T 的右值引用,它既有多是左值引用,也有多是右值引用,但必定不是拷贝原值。

int val = 0;
int &ref = val;
const int &cref = val;

auto s1 = ref;      // 拷贝了一个 int!

auto &&t1 = ref;    // int& &&        = int&
auto &&t2 = cref;   // const int& &&  = const int&
auto &&t3 = 0;      // int&& &&       = int&&

当遇到须要推导类型的状况,被推导的 auto 类型会与 && 相结合,按照以上的规则得出总的类型。由于这个特性能够不用拷贝且能够保持变量的原有引用类型,它常和移动构造函数配合进行所谓的“完美转发”:

struct CitizenRecord {
   string first, middle, last;
   template<class F, class M, class L>
   CitizenRecord(F &&first_, M &&middle_, L &&last_) // perfect forwarding!
       : first{forward<F>(first_)}, middle{forward<M>(middle_)}, last{forward<L>(last_)} {}
   template<class F, class L>
   CitizenRecord(F &&first_, L &&last_)
       : CitizenRecord{forward<F>(first_), "", forward<L>(last_)} {}
};

以上就至关于一次性把 &const &&& 的重载都写了。

显式模板初始化 (c++98)

你们都知道模板只在编译时存在。若是一个模板定义历来没有被使用过的话,那么它就没有实例,至关于历来没有定义过模板。因此咱们不能把模板的实现和声明分别放在实现文件和头文件中。不过咱们能够显式地告诉编译器实例化部分模板:

// genlib.hpp
#pragma once
template<class T> T my_max(T a, T b);
// genlib.cpp
template<class T> T my_max(T a, T b) {
   return a > b ? a : b;
}
template int my_max<int>(int, int);             // <-
template double my_max<double>(double, double); // <- 指定实例化
// user.cpp
#include <iostream>#include "genlib.hpp"using namespace std;
int main() {
   cout << my_max(4, 5) << endl;      // 5
   cout << my_max(4.5, 5.5) << endl;  // 5.5
   // cout << my_max(4, 5.5) << endl; // 错误:没有对应的重载
}

在这里我在 genlip.cpp 定义了函数模板而且指定生成了两个实例,这样它们的符号就可见于外部,链接器就能够找到相应的定义。另一个单元 user.cpp 便可引用相应的函数。

模板模板参数 (template template) (c++98)

以及它的兄弟姐妹模板x3参数,模板x4参数,......

初学者大都熟悉模板的类型参数 (template<class>) 和非类型参数 (template<auto>),不过对于模板模板参数可能会很不熟悉,由于须要这个特性的地方不多。所谓的模板模板参数实际上就表示参数自己就是个模板,好比 std::vector 是一个模板类。若是要把它传入一个接受普通的模板参数的模板中,咱们只能去实例化它,例如传入 std::vector<int>。对于模板模板参数,能够直接传入这个模板类 std::vector

template<template<class...> class GenericContainer>
struct Persons {
   GenericContainer<int> ids;
   GenericContainer<Person> people;
};

int main() {
   Persons<vector> ps;
   ps.ids.emplace_back(1);
   ps.people.emplace_back(Person{"alice"});
}

在这里 GenericContainer 就是模板模板,咱们不只可使用 vector 来初始化 Persons,还可使用任何 STL 的容器模板类。

由于参数能够套娃了,因此模板也有本身所谓的“高阶函数”

// “变量”类型
template<int v> struct i32 {
   using type = i32;
   static constexpr int value = v;
};

// 计算一个数加一
template<class I1> struct add1 : i32<I1::value + 1> {};

// 组合两个函数
template<template<class> class F, template<class> class G>
struct compose {                  // 显式标注 typename 消除歧义
   template<class I1> struct func : F<typename G<I1>::type> {};
};

int main() {
   // 加一函数组合三次就是加三
   using add3 = compose<add1, compose<add1, add1>::func>;
   // “声明”一个“变量”
   using num = i32<7>;
   cout << add3::func<num>::value << endl; // 9
}

class template deduction guide (CTAD) (c++17)

从 C++17 开始,在初始化模板类的时候能够不用标注模板参数,如这样:

vector vec1 {1,2,3,4,6};        // 等同于 vector<int>
vector vec2 {"hello", "alice"}; // 等同于 vector<const char *>

相应的类型会被自动推导。除了使用编译器默认的设置,咱们能够经过 deduction guide 定义本身的推导规则。之因此说起它,是由于这是一个你们可能会遇到的陌生语法:

struct ptr_wrapper {
   uintptr_t ptr;
   template<class T> ptr_wrapper(T *p): ptr{reinterpret_cast<uintptr_t>(p)} {}
};

template<class T> struct bad_wrapper {
   T thing;
};

// 若是参数是 const char *,则调用 bad_wrapper<string> 的构造函数。这样写没问题由于
// string 能够由 const char * 构造
bad_wrapper(const char *) -> bad_wrapper<string>;

// 若是参数是指针,则调用 bad_wrapper<ptr_wrapper>,由于我本身定义了构造函数,因此 OK
template<class T> bad_wrapper(T *) -> bad_wrapper<ptr_wrapper>;

int main() {
   bad_wrapper w {"alice"}; // bad_wrapper<string>
   cout << w.thing.size() << endl; // 5
   bad_wrapper p {&w};      // bad_wrapper<ptr_wrapper>
   cout << p.thing.ptr << endl;    // 140723957134752
}

当使用 {} 初始化对象时,编译器会查看大括号内参数的类型,若是参数类型符合推导规则的左侧,则编译器会按照箭头右侧的规则来实例化模板类。固然,这须要箭头右侧的表达式在被代入后有效,并且须要类型要能够被实际的参数构造。(en.cppreference.com/w/c)

递归模板 (c++98)

模板在必定程度上就是编译期的函数,支持递归是理所应当的。在早期,这个高级学究的 zhuangbi 利器。利用类的继承和模板的特化就能够实现不少递归和匹配操做。下面是一个不用内建数组实现的数组功能:

// 要先生成 长度为 Size 的数组,则要先在前面生成长度为 Size-1 的数组
template<class T, size_t Size> struct flex_array : flex_array<T, Size-1> {
   private: T v;
};

template<class T> struct flex_array<T, 1> {
   T &operator[](size_t i) { return *(&v + i); }
   private: T v;
};

int main() {
   flex_array<unsigned, 4> arr;
   cout << sizeof arr << endl;  // 16
}

动态类型信息 RTTI (c++98)

依赖虚函数,dynamic_cast 等工具咱们能够在程序运行时得到类型的信息而且检查。当重载 = 算符的时候,咱们要当心。

class Base {
public:
   virtual Base &operator=(const Base&) { return *this; }
};

class Derived1: public Base {
public:
   Derived1 &operator=(const Base &o) override
   try {                      // 动态类型转换!
       const Derived1 &other = dynamic_cast<const Derived1&>(o);
       if (this == &other) return *this;
       Base::operator=(other);
       return *this;
   } catch (bad_cast&) {                                        // 类型信息
       throw runtime_error{string{"type mixed! passed type: "} + typeid(o).name()};
   }
};

int main() {
   Base *p1 = new Base{};
   Base *p2 = new Derived1{};
   *p2 = *p1;  // exception: type mixed! passed type: 4Base
}

静态多态和静态内省 (c++98)

动态类型信息和虚函数等方便使用,只不过也会形成运行时的损耗。经过递归的模板,咱们有 CRTP 设计模式来实现静态的多态方法派发。

template<class Derived> class IPersonaBase {
   void speak_impl_() const { cout << "I'm unnamed." << endl; }
public:                 // 静态类型转换!
   void speak() const { static_cast<const Derived *>(this)->speak_impl_(); }
};

class Alice : public IPersonaBase<Alice> { // <- curiously recurring
   friend IPersonaBase<Alice>;
   int number = 665764;
   void speak_impl_() const { cout << "I'm alice bot #" << number << endl; }
};

class Mian : public IPersonaBase<Mian> {};

int main() {
   Alice a; Mian m;
   a.speak(); m.speak();
}
// 输出
// I'm alice bot #665764
// I'm unnamed.

CRTP 的一个特色是支持按子类的类型返回子类。这个 SO 答案描述了 CRTP 的用途 stackoverflow.com/quest

咱们没有动态内省,可是静态的内省机制也已经足够,而且性能更强。经过 SFINAE (substitution failure is not an error),用户在程序运行前就能够提早知道该使用什么重载来对付不一样的类型了。好比,咱们能够检测参数的类型有没有 serialize 方法

// 辅助函数,用来检测 T 是否是有 serialize() 方法
template<class T> struct is_serializable {
private:
   template<class TT>
   static decltype(declval<TT>().serialize(), true_type()) test(int);
   template<class TT> static false_type test(...);
public:
   static constexpr bool value = is_same_v<decltype(test<T>(0)), true_type>;
};

// 若是 T x 有 serialize 方法,则打印它的 serialized,不然打印另一条信息
template<typename T> void print_serialization(T &x) {
   if constexpr (is_serializable<T>::value)
       cout << x.serialize() << endl;
   else
       cout << "[not serializable]" << endl;
}

constraints & concepts (c++20)

对于模板参数的约束语法是 C++ 最重大的升级之一。有了 concept,用户大多数状况已经能够摆托 SFINAE 那简直不可读的代码,来定义相似于其余语言,可是性能更优的鸭子类型“接口”。

template<class T> concept ISerializable = requires(T v) {
   {v.serialize()} -> same_as<string>;
};

template<class T> concept IFileAlike = requires(T v) {
   {v.open()} -> same_as<void>;
   {v.close()} noexcept -> same_as<void>;
   {v.write(declval<string&>())} -> same_as<void>;
};

// 只有定义了上面这些函数的类型才能够 print_file
template<class T> void print_file(T &&file) requires ISerializable<T> && IFileAlike<T> {
   file.open();
   cout << file.serialize() << endl;
   file.close();
}

小编推荐一个学C语言/C++的学习裙【 712,284,705】,不管你是大牛仍是小白,是想转行仍是想入行均可以来了解一块儿进步一块儿学习!裙内有开发工具,不少干货和技术资料分享!