<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          這些課堂上不教的 C++ 的基本特性你都知道嗎?

          共 487字,需瀏覽 1分鐘

           ·

          2020-10-20 09:42

          來源:https://zhuanlan.zhihu.com/p/149839787


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

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

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

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

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

          因為數(shù)組取下標相當于計算指針地址與偏移量,而?*(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 標準庫宏中廣泛出現(xiàn)。大多數(shù)常見語言也都有這個特性。

          邏輯運算關鍵字 (c++98)

          對于布爾值運算,C++ 也提供?and,or?這樣的關鍵字,與?&&,||?等作用相同:

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

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

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

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

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

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

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

          long const int static long value = 2;

          它可以通過編譯,而且類型是?const long long。

          uniform/aggregate initialization (c++11)

          C++98 中對象和內(nèi)建類型的初始化方式五花八門,為了解決這個問題,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

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

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

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

          int z(10.1); // OK. 強轉(zhuǎn)為 10
          // int x{10.1}; // 報錯

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

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

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

          這個特性我覺得大家應該都會知道,而且它在動態(tài)語言里也很常見。和上一條一樣,其在模板中有應用。但是這個例子可以寫得更激進:

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

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

          像函數(shù)或者表達式返回一個左值 (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)還加入了分隔符,再也不用擔心數(shù)字太多眼睛看花的問題了:

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

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

          很少有人知道函數(shù),構造函數(shù),析構函數(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

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

          匿名類 (c++98)

          C 就支持匿名 (untagged) 類的定義,不過 C++ 更進一步,你可以在函數(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ù)和構造析構函數(shù)。如果仔細想想,其原理和 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?語句中也可以使用這個結(jié)構。例子中?i?的生命周期限于這個?if/else?塊中。其就相當于

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

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

          在 C++17 中,這個特性被加強了。不僅可以聲明,還可以像?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é)構化綁定 (c++17)

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

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

          這個特性可以解包標準庫中的 std::tuple,也可以以成員聲明順序解包一個結(jié)構體:

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

          int main() {
          // std::ignore 用來丟棄一個不需要的數(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?也可以實現(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)

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

          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)建對象
          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();
          // 不保證原內(nèi)存一定會被清零!
          }
          // 輸出
          // 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() {}
          }

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

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

          相同。對于匿名命名空間和 static,可以參考這個 SO 問題:?stackoverflow.com/quest

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

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

          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 的啟發(fā),可以簡便地向標準庫的 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
          }

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

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

          類成員聲明順序 (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();
          }

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

          不同于?const,noexcept?這樣僅修飾類方法本身行為的修飾符,&?和?&&?修飾符會根據(jù) *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)

          只有子類型的指針,如何調(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

          構造器委托 (c++11)

          可以使用和繼承類相同的語法在一個構造函數(shù)中調(diào)用另外一個構造函數(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";
          }
          // 默認參數(shù)委托
          CitizenRecord(string first_, string last_)
          : CitizenRecord{move(first_), "", move(last_)} {}
          // 拷貝構造函數(shù)委托
          CitizenRecord(const CitizenRecord &o)
          : CitizenRecord{o.first, o.middle, o.last} {}
          // 移動構造函數(shù)委托
          CitizenRecord(CitizenRecord &&o)
          : CitizenRecord{move(o.first), move(o.middle), move(o.last)} {}
          };

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

          自定義枚舉存儲類型 (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 的區(qū)別是前者的作用域是全局,后者則需要加上?sizes::.

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

          聯(lián)合也是 C++ 的類,也支持方法,構造和析構,同樣還有模板參數(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
          }

          一點需要注意的是,union 的成員必須是 "trivial" 的,也就是無需顯式構造函數(shù),否則輕則編譯失敗,重則 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)?代表一個接受兩個整形返回一個整形的函數(shù)指針,對于一個類型?T,int(T::*)(int, int)?表示一個非靜態(tài)的類成員函數(shù)的類型。這是因為這種函數(shù)總會有一個隱式的?this?指針作為第一個參數(shù),所以我們需要使用不同的語法來區(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ù)指針是實現(xiàn) traits 必不可少的。

          返回值后置 (c++11)

          R (Args)?等同于?auto (Args) -> R。返回值后置可以用在很多地方,如常見的 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();
          }

          一般不為人知的是,后置返回值還可以簡化函數(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;
          }
          // 不后置的話這個函數(shù)的聲明是這樣的:
          // 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
          }

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

          因為函數(shù)聲明 = 后置返回值的函數(shù)聲明,我們可以在模板參數(shù)里也使用這個語法,例如 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; 根本沒有內(nèi)存被分配!
          decltype(int{} * double{}) value {}; // 只是聲明一個變量,這個變量的類型是 int 乘 double 的類型
          // 具體是什么類型我自己不知道

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

          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!運行程序時根本不會再去計算
          }

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

          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 可以作為函數(shù)的返回值來自動推導,不過對于 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; // 報錯
          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 類型會與 && 相結(jié)合,按照以上的規(guī)則得出總的類型。因為這個特性可以不用拷貝且可以保持變量的原有引用類型,它常和移動構造函數(shù)配合進行所謂的“完美轉(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_)} {}
          };

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

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

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

          // 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 #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?定義了函數(shù)模板并且指定生成了兩個實例,這樣它們的符號就可見于外部,連接器就可以找到相應的定義。另外一個單元?user.cpp?即可引用相應的函數(shù)。

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

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

          初學者大都熟悉模板的類型參數(shù) (template) 和非類型參數(shù) (template),不過對于模板模板參數(shù)可能會很不熟悉,因為需要這個特性的地方很少。所謂的模板模板參數(shù)實際上就表示參數(shù)本身就是個模板,比如?std::vector?是一個模板類。如果要把它傳入一個接受普通的模板參數(shù)的模板中,我們只能去實例化它,例如傳入?std::vector。對于模板模板參數(shù),可以直接傳入這個模板類?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 的容器模板類。

          因為參數(shù)可以套娃了,所以模板也有自己所謂的“高階函數(shù)”

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

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

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

          int main() {
          // 加一函數(shù)組合三次就是加三
          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 開始,在初始化模板類的時候可以不用標注模板參數(shù),如這樣:

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

          相應的類型會被自動推導。除了使用編譯器默認的設置,我們可以通過 deduction guide 定義自己的推導規(guī)則。之所以提及它,是因為這是一個大家可能會遇到的陌生語法:

          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 的構造函數(shù)。這樣寫沒問題因為
          // string 可以由 const char * 構造
          bad_wrapper(const char *) -> bad_wrapper<string>;

          // 如果參數(shù)是指針,則調(diào)用 bad_wrapper,因為我自己定義了構造函數(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
          }

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

          遞歸模板 (c++98)

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

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

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

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

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

          class Derived1: public Base {
          public:
          Derived1 &operator=(const Base &o) override
          try { // 動態(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)

          動態(tài)類型信息和虛函數(shù)等方便使用,只不過也會造成運行時的損耗。通過遞歸的模板,我們有 CRTP 設計模式來實現(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 的一個特點是支持按子類的類型返回子類。這個 SO 答案描述了 CRTP 的用途?stackoverflow.com/quest

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

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

          對于模板參數(shù)的約束語法是 C++ 最重大的升級之一。有了 concept,用戶大多數(shù)情況已經(jīng)可以擺托 SFINAE 那簡直不可讀的代碼,來定義類似于其他語言,但是性能更優(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();
          }

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

          總結(jié)

          當我花了一整天邊寫邊測完成這篇文章的時候,也獲得了很大的收獲,所以我覺得這篇文章應該會對讀者有意義。文章里有的地方?jīng)]有詳細展開,如果你有興趣,不妨就按著每一條的標題去搜索!

          瀏覽 62
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产黄色片在线播放 | 男男一区二区三区 | 亚洲无码在线电影 | 亚洲另类调教 | 插逼视频网站 |