當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > C語(yǔ)言與CPP編程
[導(dǎo)讀]這篇文章列舉一些C++的用到的或多或少,但學(xué)習(xí)中幾乎都會(huì)忽視的語(yǔ)言特(lou)性(dong),希望讀者看完能有收獲。

來(lái)源:https://zhuanlan.zhihu.com/p/149839787


C++ 作為一個(gè)歷史久遠(yuǎn),功能豐(yong)富(zhong)而且標(biāo)準(zhǔn)與時(shí)俱進(jìn)的語(yǔ)言,理應(yīng)什么都能做,什么都用得起來(lái)。不過(guò)日常使用中我們初學(xué)者真的好像只學(xué)到了其中的一部分,對(duì)于一些另類的特性都不怎了解。這篇文章列舉一些 C++ 的用到的或多或少,但是學(xué)習(xí)中幾乎都會(huì)忽視的語(yǔ)言特(lou)性(dong),希望讀者看完能有收獲。如果你沒(méi)有收獲,建議去做一名語(yǔ)言律師~

以下一部分都是從 SO 的高票問(wèn)題找出的,還有一些是業(yè)余的收集,如果有紕漏敬請(qǐng)指出。當(dāng)然過(guò)于高(zhuang)級(jí)(bi)的奇技淫巧就沒(méi)有必要介紹了。以下例子純手打,在 clang 測(cè)試過(guò)。

交換數(shù)組變量和下標(biāo) (c)

如果你想獲取數(shù)組元素,可以寫?A[i]?或者是?i[A]

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

因?yàn)閿?shù)組取下標(biāo)相當(dāng)于計(jì)算指針地址與偏移量,而?*(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",即以上三行合起來(lái)。這個(gè)語(yǔ)法源于 C,在 C 標(biāo)準(zhǔn)庫(kù)宏中廣泛出現(xiàn)。大多數(shù)常見(jiàn)語(yǔ)言也都有這個(gè)特性。

邏輯運(yùn)算關(guān)鍵字 (c++98)

對(duì)于布爾值運(yùn)算,C++ 也提供?and,or?這樣的關(guān)鍵字,與?&&,||?等作用相同:

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

雙字符組 Digraph / 三字符組 Trigraph (c)

過(guò)往部分地區(qū)的人們的鍵盤不方便打出大括弧之類的特殊符號(hào),所以類似字符串的轉(zhuǎn)義字符,這些語(yǔ)法符號(hào)也可以被另外常見(jiàn)的符號(hào)所代表

%:include <iostream>
using namespace std;
int main() <%
int a<::> = <% 1,2,3 %>;
cout << a<:1:> << endl; // 2
// trigraph 寫法,必須開(kāi)啟 -trigraphs 選項(xiàng)
cout << a??(1??) << endl; // 2
%>

是可以在現(xiàn)代編譯器上編譯的。盡管不再有用,Digraph 仍然存在,不過(guò)它的兄弟 Trigraph 則很早就已經(jīng)被廢棄了。

變量類型修飾符的順序 (c)

我們知道像?const,static?等變量修飾符可以隨意交換順序,不過(guò)你有沒(méi)有想過(guò)這種情況呢?

long const int static long value = 2;

它可以通過(guò)編譯,而且類型是?const long long

uniform/aggregate initialization (c++11)

C++98 中對(duì)象和內(nèi)建類型的初始化方式五花八門,為了解決這個(gè)問(wèn)題,C++11 引入了所謂的集合初始化:

int a {};     // 默認(rèn)初始化,對(duì)于整形,初始化為 0,相當(dāng)于 bzero
int b {10}; // 初始化為 10
int f = {}, g = {50}; // !
double d {10};
const char *s {"hello"};
cout << f << ' '<< g << endl; // 0 50

這旨在使內(nèi)建非對(duì)象的類型(基礎(chǔ)類型和數(shù)組)都能像用戶定義的對(duì)象一樣以相同的語(yǔ)法被初始化。這樣就相當(dāng)于“像 int 這類的類型也有了 initializer_list 構(gòu)造函數(shù)”,這給模板函數(shù)帶來(lái)了很大的方便:

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>();
}

本文的第一個(gè)例子就用到了這個(gè)特性。統(tǒng)一和集合初始化的門道很多,建議大家查閱相關(guān)專業(yè)資料。

int z(10.1); // OK. 強(qiáng)轉(zhuǎn)為 10
// int x{10.1}; // 報(bào)錯(cuò)

函數(shù)返回 void (c)

如果一個(gè)函數(shù)的返回類型是?void,那么你是可以用?return?返回它的:

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

這個(gè)特性我覺(jué)得大家應(yīng)該都會(huì)知道,而且它在動(dòng)態(tài)語(yǔ)言里也很常見(jiàn)。和上一條一樣,其在模板中有應(yīng)用。但是這個(gè)例子可以寫得更激進(jìn):

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

表達(dá)式返回左值 (c++98)

像函數(shù)或者表達(dá)式返回一個(gè)左值 (lvalue) 是 C++ 中最基礎(chǔ)的操作,不過(guò)有時(shí)候也能玩出花兒來(lái):

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

只要等號(hào)左邊是左值,什么東西都可以放。哪怕你是有逗號(hào)運(yùn)算符,還是 lambda。

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

當(dāng)心被同事打死。

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

C++14 不僅加入了二進(jìn)制字面量的支持 (0bxxxyyyzzz)還加入了分隔符,再也不用擔(dān)心數(shù)字太多眼睛看花的問(wèn)題了:

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

函數(shù)域 try/catch (c++98)

很少有人知道函數(shù),構(gòu)造函數(shù),析構(gòu)函數(shù)可以聲明全局的異常捕獲,就像這樣:

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

對(duì)于函數(shù),在這里其作用就相當(dāng)于在?main?下多寫一層花括號(hào)。構(gòu)造函數(shù)類似,但是在?catch塊中如果用戶不拋出異常,編譯器會(huì)一定隱式拋出原異常。(en.cppreference.com/w/c)

匿名類 (c++98)

C 就支持匿名 (untagged) 類的定義,不過(guò) C++ 更進(jìn)一步,你可以在函數(shù)的任何地方定義匿名類,甚至循環(huán)變量的聲明中,如以下反轉(zhuǎn)數(shù)組的函數(shù):

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]);
}
}

匿名類也可以出現(xiàn)在?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

除此還可以定義虛函數(shù)和構(gòu)造析構(gòu)函數(shù)。如果仔細(xì)想想,其原理和 lambda 差不多。

if 語(yǔ)句中聲明變量 (c++98)

如下所示:

bool get_result() { return false; }

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

在?while?語(yǔ)句中也可以使用這個(gè)結(jié)構(gòu)。例子中?i?的生命周期限于這個(gè)?if/else?塊中。其就相當(dāng)于

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

能在作用域中聲明變量,這和 C 程序中的類似技巧很不同。

在 C++17 中,這個(gè)特性被加強(qiáng)了。不僅可以聲明,還可以像?for?循環(huán)一樣附加條件,和 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

結(jié)構(gòu)化綁定 (c++17)

C++17 的新語(yǔ)法使得我們可以在一個(gè)語(yǔ)句里解包變量,例如解包一個(gè)長(zhǎng)度為 3 的數(shù)組:

int arr[3] { 1,2,9 };
// 相當(dāng)于把數(shù)組的值都拷貝到這三個(gè)變量里
auto [cpy0, cpy1, cpy2] = arr;
cpy0 = 2;
cout << cpy0 << ' ' << arr[0] << endl; // 2 1
// 相當(dāng)于把數(shù)組的值起了三個(gè)別名
auto &[ref0, ref1, ref2] = arr;
ref0 = 2;
cout << ref0 << ' ' << arr[0] << endl; // 2 2

這個(gè)特性可以解包標(biāo)準(zhǔn)庫(kù)中的 std::tuple,也可以以成員聲明順序解包一個(gè)結(jié)構(gòu)體:

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

int main() {
// std::ignore 用來(lái)丟棄一個(gè)不需要的數(shù)據(jù)
if (auto [code, ignore, json] = get_data(); code == 200)
cout << "success: " << json << endl;
else
throw runtime_error{"request failed"};
}

在 C++17 之前,使用?std::tie?也可以實(shí)現(xiàn)相類似的效果:

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)

當(dāng)對(duì)象被 new 的時(shí)候,大家都知道發(fā)生的過(guò)程是先調(diào)用?operator new?來(lái)分配內(nèi)存,再調(diào)用構(gòu)造函數(shù)。不過(guò) new 擁有另一個(gè)重載,我們可以跳過(guò)分配內(nèi)存的一步,也就是在我們指定的內(nèi)存區(qū)域直接初始化對(duì)象。

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 上創(chuàng)建對(duì)象
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;

// 析構(gòu)對(duì)象
p->~vector2d();
// 不保證原內(nèi)存一定會(huì)被清零!
}
// 輸出
// unnamed(3, 4)
// unnamed(3, 4)

全局命名空間操作符 (c++98)

可以使用?::?來(lái)顯式表示當(dāng)前所表示的符號(hào)來(lái)自于全局命名空間,從而消除歧義:

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)

使用匿名的命名空間可以限制符號(hào)的可見(jiàn)性。當(dāng)你寫下這段代碼時(shí):

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

它相當(dāng)于這段代碼:

// 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() {}
}

也就是一個(gè)獨(dú)一無(wú)二,對(duì)于當(dāng)前編譯單元的命名空間被創(chuàng)建。因?yàn)檫@個(gè)命名空間只在這個(gè)文件中引用,故而用戶只可以在當(dāng)前文件 (test.cpp) 中引用?local_function。這樣做就避免了不同文件中可能出現(xiàn)的名稱沖突。其效果與

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

相同。對(duì)于匿名命名空間和 static,可以參考這個(gè) SO 問(wèn)題:?stackoverflow.com/quest

“內(nèi)聯(lián)” (inline) 命名空間 (c++11)

如果不是庫(kù)作者,可能會(huì)對(duì)這個(gè)特性十分驚訝,內(nèi)聯(lián)這個(gè)關(guān)鍵字的含義已經(jīng)和?static?一樣要起飛了。如果一個(gè)命名空間被內(nèi)聯(lián),那么它會(huì)被給予優(yōu)先級(jí)。

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""?來(lái)以字面量的形式初始化對(duì)象:

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

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

重載 , 運(yùn)算符 (c++98)

沒(méi)想到吧,逗號(hào)也能重載!這個(gè)例子受 boost/assign/std/vector.hpp 的啟發(fā),可以簡(jiǎn)便地向標(biāo)準(zhǔn)庫(kù)的 vector 中一個(gè)個(gè)地插入元素:

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
}

逗號(hào)操作符還有一處另類的地方是當(dāng)操作數(shù)的類型為?void?時(shí),沒(méi)有重載可以改變它的行為。這可以(在c++98)用來(lái)檢測(cè)一個(gè)表達(dá)式是否為?void:?stackoverflow.com/quest

除此之外大多數(shù)重載 , 的行為都不是什么好行為,知道能重載就好,想用的話最好三思!

類成員聲明順序 (c++98)

類成員在使用時(shí)不需要關(guān)心它們的聲明順序,所以我們可以先使用變量再定義。所以有時(shí)為了方便完全可以把所有的代碼寫在一個(gè)類里。

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();
}

類函數(shù)引用修飾符 (c++11)

不同于?const,noexcept?這樣僅修飾類方法本身行為的修飾符,&?和?&&?修飾符會(huì)根據(jù) *this 是左值還是右值引用來(lái)選擇合適的重載。

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)

只有子類型的指針,如何調(diào)用父類型的方法?用命名空間。

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

構(gòu)造器委托 (c++11)

可以使用和繼承類相同的語(yǔ)法在一個(gè)構(gòu)造函數(shù)中調(diào)用另外一個(gè)構(gòu)造函數(shù):

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";
}
// 默認(rèn)參數(shù)委托
CitizenRecord(string first_, string last_)
: CitizenRecord{move(first_), "", move(last_)} {}
// 拷貝構(gòu)造函數(shù)委托
CitizenRecord(const CitizenRecord &o)
: CitizenRecord{o.first, o.middle, o.last} {}
// 移動(dòng)構(gòu)造函數(shù)委托
CitizenRecord(CitizenRecord &&o)
: CitizenRecord{move(o.first), move(o.middle), move(o.last)} {}
};

這個(gè)特性有時(shí)可以適量減少代碼重復(fù),或者轉(zhuǎn)發(fā)默認(rèn)參數(shù)。

自定義枚舉存儲(chǔ)類型 (c++11)

C 時(shí)代的枚舉中定義中的整形都必須是?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 的區(qū)別是前者的作用域是全局,后者則需要加上?sizes::.

模板聯(lián)合 (c++98)

聯(lián)合也是 C++ 的類,也支持方法,構(gòu)造和析構(gòu),同樣還有模板參數(shù)。

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
}

一點(diǎn)需要注意的是,union 的成員必須是 "trivial" 的,也就是無(wú)需顯式構(gòu)造函數(shù),否則輕則編譯失敗,重則 UB (未定義行為)。

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

你可能很熟悉 C/C++ 的位域特性,但是你知道位域可以寫進(jìn)模板嗎?

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)?代表一個(gè)接受兩個(gè)整形返回一個(gè)整形的函數(shù)指針,對(duì)于一個(gè)類型?Tint(T::*)(int, int)?表示一個(gè)非靜態(tài)的類成員函數(shù)的類型。這是因?yàn)檫@種函數(shù)總會(huì)有一個(gè)隱式的?this?指針作為第一個(gè)參數(shù),所以我們需要使用不同的語(yǔ)法來(lái)區(qū)分它們。

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; // 類靜態(tài)方法指針 (就是普通函數(shù)指針)
void (&smref)(string&) = echoer::echo2; // 類靜態(tài)方法引用

(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!

成員指針和函數(shù)指針是實(shí)現(xiàn) traits 必不可少的。

返回值后置 (c++11)

R (Args)?等同于?auto (Args) -> R。返回值后置可以用在很多地方,如常見(jiàn)的 lambda:

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

以及在一般的函數(shù)中表示返回類型依賴于參數(shù):

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

一般不為人知的是,后置返回值還可以簡(jiǎn)化函數(shù)指針的寫法。

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;
}
// 不后置的話這個(gè)函數(shù)的聲明是這樣的:
// int (*(produce(int op)))(int, int);

int main() {
using producer_t = auto (*)(int) -> auto (*)(int, int) -> int; // 指針后置
// 不后置的話這個(gè)類型的聲明是這樣的
// using producer_t = int (*((*)(int)))(int, int);
producer_t ptr {&produce};
cout << ptr(0)(1, 2) << endl; // 3
}

類成員函數(shù)也類似地可以這樣寫。在 C++14 中函數(shù)聲明的返回值可以只寫?auto?來(lái)讓編譯器自動(dòng)推導(dǎo)返回類型了。

因?yàn)楹瘮?shù)聲明 = 后置返回值的函數(shù)聲明,我們可以在模板參數(shù)里也使用這個(gè)語(yǔ)法,例如 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

不求值表達(dá)式和編譯期常量 (c++98)

在 C 時(shí)代就有僅存在于編譯期的表達(dá)式。在程序編譯運(yùn)行后,這些代碼就被刪除,替換成常量了。比如常見(jiàn)的 sizeof 運(yùn)算符

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

因?yàn)?sizeof(count++)?直接在編譯期被替換成了 4,其所原本應(yīng)該具有的副作用也會(huì)沒(méi)有作用。
在 C++11 中引入了 decltype,它和 noexcept, sizeof 操作符相同,僅存在于編譯時(shí)期。而這個(gè)操作符的作用是獲得一個(gè)表達(dá)式的類型,譬如如此:

decltype(new int[10]) ptr {};        // 等同于 int *ptr = nullptr; 根本沒(méi)有內(nèi)存被分配!
decltype(int{} * double{}) value {}; // 只是聲明一個(gè)變量,這個(gè)變量的類型是 int 乘 double 的類型
// 具體是什么類型我自己不知道

與其一同來(lái)臨的還有 declval 等。同時(shí)還有 constexpr 表達(dá)式,可以保證表達(dá)式一定會(huì)在編譯期內(nèi)計(jì)算完畢,不拖延到運(yùn)行時(shí)。

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!運(yùn)行程序時(shí)根本不會(huì)再去計(jì)算
}

實(shí)際上于此我們可以解釋憑什么有的函數(shù)沒(méi)有定義就能跑:

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
}

因?yàn)樗鼈兏緵](méi)跑。

decltype(auto) (c++14)

之前講過(guò) auto 可以作為函數(shù)的返回值來(lái)自動(dòng)推導(dǎo),不過(guò)對(duì)于 auto 和 decltype(auto),雖然大多數(shù)情況下后者是累贅,也存在兩者意義不同情況:

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; // 報(bào)錯(cuò)
cout << incr2(incr2(b)) << endl; // 輸出 2
}

auto 會(huì)看所要推導(dǎo)的變量原生類型?T,而 decltype(auto) 會(huì)推導(dǎo)出變量的實(shí)際類型?T&?或是?T&&。

引用折疊和萬(wàn)能引用 (universal reference) (c++11)

T&&?不一定是?T?的右值引用,它既有可能是左值引用,也有可能是右值引用,但一定不是拷貝原值。

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

auto s1 = ref; // 拷貝了一個(gè) int!

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

當(dāng)遇到需要推導(dǎo)類型的情況,被推導(dǎo)的 auto 類型會(huì)與 && 相結(jié)合,按照以上的規(guī)則得出總的類型。因?yàn)檫@個(gè)特性可以不用拷貝且可以保持變量的原有引用類型,它常和移動(dòng)構(gòu)造函數(shù)配合進(jìn)行所謂的“完美轉(zhuǎn)發(fā)”:

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_)} {}
};

以上就相當(dāng)于一次性把?&,?const &,?&&?的重載都寫了。

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

大家都知道模板只在編譯時(shí)存在。如果一個(gè)模板定義從來(lái)沒(méi)有被使用過(guò)的話,那么它就沒(méi)有實(shí)例,相當(dāng)于從來(lái)沒(méi)有定義過(guò)模板。所以我們不能把模板的實(shí)現(xiàn)和聲明分別放在實(shí)現(xiàn)文件和頭文件中。不過(guò)我們可以顯式地告訴編譯器實(shí)例化部分模板:

// 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); // <- 指定實(shí)例化
// user.cpp
#include #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; // 錯(cuò)誤:沒(méi)有對(duì)應(yīng)的重載
}

在這里我在?genlip.cpp?定義了函數(shù)模板并且指定生成了兩個(gè)實(shí)例,這樣它們的符號(hào)就可見(jiàn)于外部,連接器就可以找到相應(yīng)的定義。另外一個(gè)單元?user.cpp?即可引用相應(yīng)的函數(shù)。

模板模板參數(shù) (template template) (c++98)

以及它的兄弟姐妹模板x3參數(shù),模板x4參數(shù),......

初學(xué)者大都熟悉模板的類型參數(shù) (template) 和非類型參數(shù) (template),不過(guò)對(duì)于模板模板參數(shù)可能會(huì)很不熟悉,因?yàn)樾枰@個(gè)特性的地方很少。所謂的模板模板參數(shù)實(shí)際上就表示參數(shù)本身就是個(gè)模板,比如?std::vector?是一個(gè)模板類。如果要把它傳入一個(gè)接受普通的模板參數(shù)的模板中,我們只能去實(shí)例化它,例如傳入?std::vector。對(duì)于模板模板參數(shù),可以直接傳入這個(gè)模板類?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?來(lái)初始化?Persons,還可以使用任何 STL 的容器模板類。

因?yàn)閰?shù)可以套娃了,所以模板也有自己所謂的“高階函數(shù)”

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

// 計(jì)算一個(gè)數(shù)加一
template<class I1> struct add1 : i32<I1::value + 1> {};

// 組合兩個(gè)函數(shù)
template<template<class> class F, template<class> class G>
struct compose { // 顯式標(biāo)注 typename 消除歧義
template<class I1> struct func : F<typename G<I1>::type> {};
};

int main() {
// 加一函數(shù)組合三次就是加三
using add3 = compose<add1, compose<add1, add1>::func>;
// “聲明”一個(gè)“變量”
using num = i32<7>;
cout << add3::func<num>::value << endl; // 9
}

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

從 C++17 開(kāi)始,在初始化模板類的時(shí)候可以不用標(biāo)注模板參數(shù),如這樣:

vector vec1 {1,2,3,4,6};        // 等同于 vector
vector vec2 {"hello", "alice"}; // 等同于 vector

相應(yīng)的類型會(huì)被自動(dòng)推導(dǎo)。除了使用編譯器默認(rèn)的設(shè)置,我們可以通過(guò) deduction guide 定義自己的推導(dǎo)規(guī)則。之所以提及它,是因?yàn)檫@是一個(gè)大家可能會(huì)遇到的陌生語(yǔ)法:

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;
};

// 如果參數(shù)是 const char *,則調(diào)用 bad_wrapper 的構(gòu)造函數(shù)。這樣寫沒(méi)問(wèn)題因?yàn)?br>// string 可以由 const char * 構(gòu)造
bad_wrapper(const char *) -> bad_wrapper<string>;

// 如果參數(shù)是指針,則調(diào)用 bad_wrapper,因?yàn)槲易约憾x了構(gòu)造函數(shù),所以 OK
template<class T> bad_wrapper(T *) -> bad_wrapper<ptr_wrapper>;

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

當(dāng)使用?{}?初始化對(duì)象時(shí),編譯器會(huì)查看大括號(hào)內(nèi)參數(shù)的類型,如果參數(shù)類型符合推導(dǎo)規(guī)則的左側(cè),則編譯器會(huì)按照箭頭右側(cè)的規(guī)則來(lái)實(shí)例化模板類。當(dāng)然,這需要箭頭右側(cè)的表達(dá)式在被代入后有效,而且需要類型要可以被實(shí)際的參數(shù)構(gòu)造。(en.cppreference.com/w/c)

遞歸模板 (c++98)

模板在一定程度上就是編譯期的函數(shù),支持遞歸是理所應(yīng)當(dāng)?shù)摹T谠缙?,這個(gè)高級(jí)學(xué)究的 zhuangbi 利器。利用類的繼承和模板的特化就可以實(shí)現(xiàn)很多遞歸和匹配操作。下面是一個(gè)不用內(nèi)建數(shù)組實(shí)現(xiàn)的數(shù)組功能:

// 要先生成 長(zhǎng)度為 Size 的數(shù)組,則要先在前面生成長(zhǎng)度為 Size-1 的數(shù)組
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
}

動(dòng)態(tài)類型信息 RTTI (c++98)

依賴虛函數(shù),dynamic_cast?等工具我們可以在程序運(yùn)行時(shí)獲得類型的信息并且檢查。當(dāng)重載 = 算符的時(shí)候,我們要小心。

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

class Derived1: public Base {
public:
Derived1 &operator=(const Base &o) override
try { // 動(dòng)態(tài)類型轉(zhuǎn)換!
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
}

靜態(tài)多態(tài)和靜態(tài)內(nèi)省 (c++98)

動(dòng)態(tài)類型信息和虛函數(shù)等方便使用,只不過(guò)也會(huì)造成運(yùn)行時(shí)的損耗。通過(guò)遞歸的模板,我們有 CRTP 設(shè)計(jì)模式來(lái)實(shí)現(xiàn)靜態(tài)的多態(tài)方法派發(fā)。

template<class Derived> class IPersonaBase {
void speak_impl_() const { cout << "I'm unnamed." << endl; }
public: // 靜態(tài)類型轉(zhuǎn)換!
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 的一個(gè)特點(diǎn)是支持按子類的類型返回子類。這個(gè) SO 答案描述了 CRTP 的用途?stackoverflow.com/quest

我們沒(méi)有動(dòng)態(tài)內(nèi)省,但是靜態(tài)的內(nèi)省機(jī)制也已經(jīng)足夠,并且性能更強(qiáng)。通過(guò) SFINAE (substitution failure is not an error),用戶在程序運(yùn)行前就可以提前知道該使用什么重載來(lái)對(duì)付不同的類型了。比如,我們可以檢測(cè)參數(shù)的類型有沒(méi)有?serialize?方法

// 輔助函數(shù),用來(lái)檢測(cè) 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)

對(duì)于模板參數(shù)的約束語(yǔ)法是 C++ 最重大的升級(jí)之一。有了 concept,用戶大多數(shù)情況已經(jīng)可以擺托 SFINAE 那簡(jiǎn)直不可讀的代碼,來(lái)定義類似于其他語(yǔ)言,但是性能更優(yōu)的鴨子類型“接口”。

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>;
};

// 只有定義了上面這些函數(shù)的類型才可以 print_file
template<class T> void print_file(T &&file) requires ISerializable<T> && IFileAlike<T> {
file.open();
cout << file.serialize() << endl;
file.close();
}

我猜很快大家就會(huì)用到了!詳細(xì)內(nèi)容請(qǐng)參考?en.cppreference.com/w/c

總結(jié)

當(dāng)我花了一整天邊寫邊測(cè)完成這篇文章的時(shí)候,也獲得了很大的收獲,所以我覺(jué)得這篇文章應(yīng)該會(huì)對(duì)讀者有意義。文章里有的地方?jīng)]有詳細(xì)展開(kāi),如果你有興趣,不妨就按著每一條的標(biāo)題去搜索!

免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國(guó)汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開(kāi)發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來(lái)越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來(lái)越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開(kāi)幕式在貴陽(yáng)舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語(yǔ)權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營(yíng)業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤(rùn)率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長(zhǎng) 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營(yíng)商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國(guó)電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長(zhǎng)三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱"軟通動(dòng)力")與長(zhǎng)三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉