主頁 > 後端開發 > 深度閱讀:大學生課外知識補充,這些課堂上不教的 C++ 的基本特性你都知道嗎?

深度閱讀:大學生課外知識補充,這些課堂上不教的 C++ 的基本特性你都知道嗎?

2020-10-27 01:20:41 後端開發

來源:知乎 王師傅的專欄

 

C++ 作為一個歷史久遠,功能豐(yong)富(zhong)而且標準與時俱進的語言,理應什么都能做,什么都用得起來,不過日常使用中我們初學者真的好像只學到了其中的一部分,對于一些另類的特性都不怎了解,這篇文章列舉一些 C++ 的用到的或多或少,但是學習中幾乎都會忽視的語言特(lou)性(dong),希望讀者看完能有識訓,如果你沒有識訓,建議去做一名語言律師~

以下一部分都是從 SO 的高票問題找出的,還有一些是業余的收集,如果有紕漏敬請指出,當然過于高(zhuang)級(bi)的奇技淫巧就沒有必要介紹了,以下例子純手打,在 clang 測驗過,

交換陣列變數和下標 (c)

如果你想獲取陣列元素,可以寫 A[i] 或者是 i[A]

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 這樣的關鍵字,與 &&|| 等作用相同:

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

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

過往部分地區的人們的鍵盤不方便打出大括弧之類的特殊符號,所以類似字串的轉義字符,這些語法符號也可以被另外常見的符號所代表

%: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 = https://www.cnblogs.com/zm131417-/p/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 = https://www.cnblogs.com/zm131417-/p/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 = https://www.cnblogs.com/zm131417-/p/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 = https://www.cnblogs.com/zm131417-/p/is_same_v(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】,無論你是大牛還是小白,是想轉行還是想入行都可以來了解一起進步一起學習!裙內有開發工具,很多干貨和技術資料分享!

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/192814.html

標籤:C

上一篇:自學編程,看書還是視頻?

下一篇:【Flutter 混合開發】與原生通信-EventChannel

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more