自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

C++常見避坑指南

開發(fā)
本文主要總結(jié)了在C++開發(fā)或review過程中常見易出錯(cuò)點(diǎn)做了歸納總結(jié),希望借此能增進(jìn)大家對(duì)C++的了解,減少編程出錯(cuò),提升工作效率,也可以作為C++開發(fā)的避坑攻略。

作者 | gouglegou

C++ 從入門到放棄?本文主要總結(jié)了在C++開發(fā)或review過程中常見易出錯(cuò)點(diǎn)做了歸納總結(jié),希望借此能增進(jìn)大家對(duì)C++的了解,減少編程出錯(cuò),提升工作效率,也可以作為C++開發(fā)的避坑攻略。

一、空指針調(diào)用成員函數(shù)會(huì)crash??

當(dāng)調(diào)用一個(gè)空指針?biāo)赶虻念惖某蓡T函數(shù)時(shí),大多數(shù)人的反應(yīng)都是程序會(huì)crash。空指針并不指向任何有效的內(nèi)存地址,所以在調(diào)用成員函數(shù)時(shí)會(huì)嘗試訪問一個(gè)不存在的內(nèi)存地址,從而導(dǎo)致程序崩潰。

事實(shí)上有點(diǎn)出乎意料,先來看段代碼:

class MyClass {
public:
  static void Test_Func1() {
    cout << "Handle Test_Func1!" << endl;
  }
  void Test_Func2() {
    cout << "Handle Test_Func2!" << endl;
  }
  void Test_Func3() {
    cout << "Handle Test_Func3! value:" << value << endl;
  }
  virtual void Test_Func4() {
    cout << "Handle Test_Func4!" << endl;
  }
  int value = 0;
};

int main() {
  MyClass* ptr = nullptr;
  ptr->Test_Func1(); // ok, print Handle Test_Func1!
  ptr->Test_Func2(); // ok, print Handle Test_Func2!
  ptr->Test_Func3(); // crash
  ptr->Test_Func4(); // crash  return 0;
}

上面例子中,空指針對(duì)Test_Func1和Test_Func2的調(diào)用正常,對(duì)Test_Func3和Test_Func4的調(diào)用會(huì)crash。可能很多人反應(yīng)都會(huì)crash,實(shí)際上并沒有,這是為啥?

類的成員函數(shù)并不與具體對(duì)象綁定,所有的對(duì)象共用同一份成員函數(shù)體,當(dāng)程序被編譯后,成員函數(shù)的地址即已確定,這份共有的成員函數(shù)體之所以能夠把不同對(duì)象的數(shù)據(jù)區(qū)分開來,靠的是隱式傳遞給成員函數(shù)的this指針,成員函數(shù)中對(duì)成員變量的訪問都是轉(zhuǎn)化成"this->數(shù)據(jù)成員"的方式。因此,從這一角度說,成員函數(shù)與普通函數(shù)一樣,只是多了this指針。而類的靜態(tài)成員函數(shù)只能訪問靜態(tài)成員變量,不能訪問非靜態(tài)成員變量,所以靜態(tài)成員函數(shù)不需要this指針作為隱式參數(shù)。

因此,Test_Func1是靜態(tài)成員函數(shù),不需要this指針,所以即使ptr是空指針,也不影響對(duì)Test_Fun1的正常調(diào)用。Test_Fun2雖然需要傳遞隱式指針,但是函數(shù)體中并沒有使用到這個(gè)隱式指針,所以ptr為空也不影響對(duì)Test_Fun2的正常調(diào)用。Test_Fun3就不一樣了,因?yàn)楹瘮?shù)中使用到了非靜態(tài)的成員變量,對(duì)num的調(diào)用被轉(zhuǎn)化成this->num,也就是ptr->num,而ptr是空指針,因此會(huì)crash。Test_Fun4是虛函數(shù),有虛函數(shù)的類會(huì)有一個(gè)成員變量,即虛表指針,當(dāng)調(diào)用虛函數(shù)時(shí),會(huì)使用虛表指針,對(duì)虛表指針的使用也是通過隱式指針使用的,因此Test_Fun4的調(diào)用也會(huì)crash。

同理,以下std::shared_ptr的調(diào)用也是如此,日常開發(fā)需要注意,記得加上判空。

std::shared_ptr<UrlHandler> url_handler;
...
if(url_handler->IsUrlNeedHandle(data)) {
  url_handler->HandleUrl(param);
}

二、字符串相關(guān)

1.字符串查找

對(duì)字符串進(jìn)行處理是一個(gè)很常見的業(yè)務(wù)場景,其中字符串查找也是非常常見的,但是用的不好也是會(huì)存在各種坑。常見的字符串查找方法有:std::string::find、std::string::find_first_of、std::string::find_first_not_of、std::string::find_last_of,各位C++ Engineer都能熟練使用了嗎?先來段代碼瞧瞧:

bool IsBlacklistDllFromSrv(const std::string& dll_name) {
    try {
        std::string target_str = dll_name;
 std::transform(target_str.begin(), target_str.end(), target_str.begin(), ::tolower);
        if (dll_blacklist_from_srv.find(target_str) != std::string::npos) {
            return true;
        }
    }
    catch (...) {
    }
    return false;
}

上面這段代碼,看下來沒啥問題的樣子。但是仔細(xì)看下來,就會(huì)發(fā)現(xiàn)字符串比對(duì)這里邏輯不夠嚴(yán)謹(jǐn),存在很大的漏洞。std::string::find只是用來在字符串中查找指定的子字符串,只要包含該子串就符合,如果dll_blacklist_from_srv = "abcd.dll;hhhh.dll;test.dll" 是這樣的字符串,傳入d.dll、hh.dll、dll;test.dll也會(huì)命中邏輯,明顯是不太符合預(yù)期的。

這里順帶回顧下C++ std::string常見的字符串查找的方法:

  • std::string::find 用于在字符串中查找指定的子字符串。如果找到了子串,則返回子串的起始位置,否則返回std::string::npos。用于各種字符串操作,例如判斷子字符串是否存在、獲取子字符串的位置等。通過結(jié)合其他成員函數(shù)和算法,可以實(shí)現(xiàn)更復(fù)雜的字符串處理邏輯。
  • std::string::find_first_of 用于查找字符串中第一個(gè)與指定字符集合中的任意字符匹配的字符,并返回其位置??捎脕頇z查字符串中是否包含指定的某些字符或者查找字符串中第一個(gè)出現(xiàn)的特定字符
  • std::string::find_first_not_of 用于查找字符串中第一個(gè)不與指定字符集合中的任何字符匹配的字符,并返回其位置。
  • std::string::find_last_of 用于查找字符串中最后一個(gè)與指定字符集合中的任意字符匹配的字符,并返回其位置。可以用來檢查字符串中是否包含指定的某些字符,或者查找字符串中最后一個(gè)出現(xiàn)的特定字符
  • std::string::find_last_not_of 用于查找字符串中最后一個(gè)不與指定字符集合中的任何字符匹配的字符,并返回其位置。

除了以上幾個(gè)方法外,還有查找滿足指定條件的元素std::find_if。

std::find_if 是 C++ 標(biāo)準(zhǔn)庫中的一個(gè)算法函數(shù),用于在指定范圍內(nèi)查找第一個(gè)滿足指定條件的元素,并返回其迭代器。需要注意的是,使用 std::find_if 函數(shù)時(shí)需要提供一個(gè)可調(diào)用對(duì)象(例如 lambda 表達(dá)式或函數(shù)對(duì)象),用于指定查找條件。

std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = std::find_if(vec.begin(), vec.end(), [](int x) { return x % 2 == 0; });
if (it != vec.end()) {
    std::cout << "Found even number: " << *it << std::endl;
}

此外,在業(yè)務(wù)開發(fā)有時(shí)候也會(huì)遇到需要C++ boost庫支持的starts_with、ends_with。如果用C++標(biāo)準(zhǔn)庫來實(shí)現(xiàn),常規(guī)編寫方法可如下:

bool starts_with(const std::string& str, const std::string& prefix) {
    return str.compare(0, prefix.length(), prefix) == 0;
}
bool ends_with(const std::string& str, const std::string& suffix) {
    if (str.length() < suffix.length()) {
        return false;
    } else {
        return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
    }
}

以上代碼中,starts_with 函數(shù)和 ends_with 函數(shù)分別用于檢查字符串的前綴和后綴。兩個(gè)函數(shù)內(nèi)部都使用了 std::string::compare 方法來比較字符串的子串和指定的前綴或后綴是否相等。如果相等,則說明字符串滿足條件,返回 true;否則返回 false。

2.std::string與std::wstring轉(zhuǎn)換

對(duì)字符串進(jìn)行處理是一個(gè)很常見的業(yè)務(wù)場景,尤其是C++客戶端開發(fā),我們經(jīng)常需要在窄字符串std::string與寬字符串std::wstring之間進(jìn)行轉(zhuǎn)換,有時(shí)候一不小心就會(huì)出現(xiàn)各種中文亂碼。還有就是一提到窄字符串與寬字符串互轉(zhuǎn)以及時(shí)不時(shí)出現(xiàn)的中文亂碼,很多人就犯暈。

在 C++ 中,std::string和std::wstring之間的轉(zhuǎn)換涉及到字符編碼的轉(zhuǎn)換。如果在轉(zhuǎn)換過程中出現(xiàn)亂碼,可能是由于字符編碼不匹配導(dǎo)致的。要正確地進(jìn)行std::string 和 std::wstring之間的轉(zhuǎn)換,需要確保源字符串的字符編碼和目標(biāo)字符串的字符編碼一致,避免C++中的字符串處理亂碼,可以使用Unicode編碼(如UTF-8、UTF-16或UTF-32)來存儲(chǔ)和處理字符串。

我們想要處理或解析一些Unicode數(shù)據(jù),例如從Windows REG文件讀取,使用std::wstring變量更能方便的處理它們。例如:std::wstring ws=L"中國a"(6個(gè)八位字節(jié)內(nèi)存:0x4E2D 0x56FD 0x0061),我們可以使用ws[0]獲取字符“中”,使用ws[1]獲取字符“國”,使用ws[2]獲取字符“國”獲取字符 'a' 等,這個(gè)時(shí)候如果使用std::string,ws[0]拿出來的就是亂碼。

此外還受代碼頁編碼的影響(比如VS可以通過文件->高級(jí)保存選項(xiàng)->編碼 來更改當(dāng)前代碼頁的編碼)。

下面是一些示例代碼,演示了如何進(jìn)行正確的轉(zhuǎn)換,針對(duì)Windows平臺(tái),官方提供了相應(yīng)的系統(tǒng)Api(MultiByteToWideChar):

std::wstring Utf8ToUnicode(const std::string& str) {
    int len = str.length();
    if (0 == len)
      return L"";
    int nLength = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), len, 0, 0);
    std::wstring buf(nLength + 1, L'\0');
    MultiByteToWideChar(CP_UTF8, 0, str.c_str(), len, &buf[0], nLength);
    buf.resize(wcslen(buf.c_str()));
    return buf;
}
std::string UnicodeToUtf8(const std::wstring& wstr) {
    if (wstr.empty()) {
      return std::string();
    }
    int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], static_cast<int>(wstr.size()), nullptr, 0, nullptr, nullptr);
    std::string str_to(size_needed, 0);
    WideCharToMultiByte(CP_UTF8, 0, &wstr[0], static_cast<int>(wstr.size()), &str_to[0], size_needed, nullptr, nullptr);
    return str_to;
}

如果使用C++標(biāo)準(zhǔn)庫來實(shí)現(xiàn),常規(guī)寫法可以參考下面:

#include <iostream>
#include <string>
#include <locale>
#include <codecvt>
// 從窄字符串到寬字符串的轉(zhuǎn)換
std::wstring narrowToWide(const std::string& narrowStr) {
    try {
        std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
        return converter.from_bytes(narrowStr);
    } catch (...) { // 如果傳進(jìn)來的字符串不是utf8編碼的,這里會(huì)拋出std::range_error異常
        return {};
    }
}
// 從寬字符串到窄字符串的轉(zhuǎn)換
std::string wideToNarrow(const std::wstring& wideStr) {
    try {
        std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
        return converter.to_bytes(wideStr);
    } catch (...) {
        return {};
    }
}
//utf8字符串轉(zhuǎn)成string
std::string utf8ToString(const char8_t* str) {
    std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
    std::u16string u16str = convert.from_bytes(
        reinterpret_cast<const char*>(str),
        reinterpret_cast<const char*>(str + std::char_traits<char8_t>::length(str)));
    return std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>{}.to_bytes(u16str);
}
int main(){
    {
        std::wstring wideStr = L"Hello, 你好!";
        std::string narrowStr = wideToNarrow(wideStr);
        std::wstring convertedWideStr = narrowToWide(narrowStr);
    } {
        //std::string narrowStr = "Hello, 你好!"; (1)
        std::string narrowStr = utf8ToString(u8"Hello, 你好!"); //(2)
        std::wstring wideStr = narrowToWide(narrowStr);
        std::string convertedNarrowStr = wideToNarrow(wideStr);
    }    
    return 0;
}

(1)首先std::string不理解編碼,在CPP官方手冊(cè)里面也解釋了,std::string處理字節(jié)的方式與所使用的編碼無關(guān),如果用于處理多字節(jié)或可變長度字符的序列(例如 UTF-8),則此類的所有成員以及它的迭代器仍然以字節(jié)(而不是實(shí)際的編碼字符)為單位進(jìn)行操作,如果用來處理包含中文的字符串就可能出現(xiàn)亂碼。這里直接將包含中文的字符串賦值給std::string,無法保證是UTF8編碼,進(jìn)行轉(zhuǎn)換時(shí)會(huì)提示std::range_error異常;此外,std::wstring是會(huì)理解編碼的,其中的字符串通常使用 UTF-16 或 UTF-32 編碼,這取決于操作系統(tǒng)和編譯器的實(shí)現(xiàn)。

(2)這里由于使用u8""構(gòu)造了UTF8編碼字符串,但是不能直接用來構(gòu)造std::string,所以進(jìn)行轉(zhuǎn)了下utf8ToString;

3.全局靜態(tài)對(duì)象

大家有沒有在工程代碼中發(fā)現(xiàn)有下面這種寫法,將常量字符串聲明為靜態(tài)全局的。

  • static const std::string kVal="hahahhaha";
  • static const std::wstring kxxConfigVal="hahahhaha";

優(yōu)點(diǎn):

  • 可讀性好:使用有意義的變量名,可以清晰地表達(dá)變量的含義和用途,提高了代碼的可讀性。
  • 安全性高:由于使用了 const 關(guān)鍵字,這個(gè)字符串變量是不可修改的,可以避免意外的修改和安全問題。
  • 生命周期長:靜態(tài)變量的生命周期從程序啟動(dòng)到結(jié)束,不受函數(shù)的調(diào)用和返回影響。

缺點(diǎn):

  • 構(gòu)造開銷:靜態(tài)變量的初始化發(fā)生在程序啟動(dòng)時(shí)也就是執(zhí)行main()之前,會(huì)增加程序啟動(dòng)的時(shí)間和資源消耗。大量的這種靜態(tài)全局對(duì)象,會(huì)拖慢程序啟動(dòng)速度
  • 靜態(tài)變量共享:靜態(tài)變量在整個(gè)程序中只有一份實(shí)例,可能會(huì)導(dǎo)致全局狀態(tài)共享和難以調(diào)試的問題。

此外,靜態(tài)變量的初始化順序可能會(huì)受到編譯單元(源文件)中其他靜態(tài)變量初始化順序的影響,因此在跨編譯單元的情況下,靜態(tài)變量的初始化順序可能是不確定的。

在實(shí)際編程中,還是不太建議使用全局靜態(tài)對(duì)象,建議的寫法:

要聲明全局的常量字符串,可以使用 const 關(guān)鍵字和 extern 關(guān)鍵字的組合:

// constants.h
extern const char* GLOBAL_STRING;
// constants.cpp
\#include "constants.h"
const char* GLOBAL_STRING = "Hello, world!";
constexpr char* kVal="hahhahah";


使用 constexpr 關(guān)鍵字來聲明全局的常量字符串:
// constants.h
constexpr const char* GLOBAL_STRING = "Hello, world!";

三、迭代器刪除

在處理緩存時(shí),容器元素的增刪查改是很常見的,通過迭代器去刪除容器(vector/map/set/unordered_map/list)元素也是常有的,但這其中使用不當(dāng)也會(huì)存在很多坑。

std::vector<int> numbers = { 88, 101, 56, 203, 72, 135 };
auto it = std::find_if(numbers.begin(), numbers.end(), [](int num) {
    return num > 100 && num % 2 != 0;
});
vec.erase(it);

上面代碼,查找std::vector中大于 100 并且為奇數(shù)的整數(shù)并將其刪除。std::find_if 將從容器的開頭開始查找,直到找到滿足條件的元素或者遍歷完整個(gè)容器,并返回迭代器it,然后去刪除該元素。但是這里沒有判斷it為空的情況,直接就erase了,如果erase一個(gè)空的迭代器會(huì)引發(fā)crash。很多新手程序員會(huì)犯這樣的錯(cuò)誤,隨時(shí)判空是個(gè)不錯(cuò)的習(xí)慣。

刪除元素不得不講下std::remove 和 std::remove_if,用于從容器中移除指定的元素, 函數(shù)會(huì)將符合條件的元素移動(dòng)到容器的末尾,并返回指向新的末尾位置之后的迭代器,最后使用容器的erase來擦除從新的末尾位置開始的元素。

std::vector<std::string> vecs = { "A", "", "B", "", "C", "hhhhh", "D" };
vecs.erase(std::remove(vecs.begin(), vecs.end(), ""), vecs.end());

// 移除所有偶數(shù)元素
vec.erase(std::remove_if(vec.begin(), vec.end(), [](int x) { return x % 2 == 0; }), vec.end());

這里的erase不用判空,其內(nèi)部實(shí)現(xiàn)已經(jīng)有判空處理。

_CONSTEXPR20 iterator erase(const_iterator _First, const_iterator _Last) noexcept(
        is_nothrow_move_assignable_v<value_type>) /* strengthened */ {
    const pointer _Firstptr = _First._Ptr;
    const pointer _Lastptr  = _Last._Ptr;
    auto& _My_data          = _Mypair._Myval2;
    pointer& _Mylast        = _My_data._Mylast;
    // ....
    if (_Firstptr != _Lastptr) { // something to do, invalidate iterators
        _Orphan_range(_Firstptr, _Mylast);
        const pointer _Newlast = _Move_unchecked(_Lastptr, _Mylast, _Firstptr);
        _Destroy_range(_Newlast, _Mylast, _Getal());
        _Mylast = _Newlast;
    }
    return iterator(_Firstptr, _STD addressof(_My_data));
}

此外,STL容器的刪除也要小心迭代器失效,先來看個(gè)vector、list、map刪除的例子:

// vector、list、map遍歷并刪除偶數(shù)元素
std::vector<int> elements = { 1, 2, 3, 4, 5 };
for (auto it = elements.begin(); it != elements.end();) {
 if (*it % 2 == 0) {
        elements.erase(it++);
    } else {
        it++;
    }
}
// Error
std::list<int> cont{ 88, 101, 56, 203, 72, 135 };
for (auto it = cont.begin(); it != cont.end(); ) {
    if (*it % 2 == 0) {
        cont.erase(it++);
    } else {
        it++;
    }
}
// Ok
 std::map<int, std::string> myMap = { {1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}, {5, "five"} };
// 遍歷并刪除鍵值對(duì),刪除鍵為偶數(shù)的元素
for (auto it = myMap.begin(); it != myMap.end(); ) {
    if (it->first % 2 == 0) {
        myMap.erase(it++);
    } else {
        it++;
    }
}
// Ok

上面幾類容器同樣的遍歷刪除元素,只有vector報(bào)錯(cuò)crash了,map和list都能正常運(yùn)行。其實(shí)vector調(diào)用erase()方法后,當(dāng)前位置到容器末尾元素的所有迭代器全部失效了,以至于不能再使用。

迭代器的失效問題:對(duì)容器的操作影響了元素的存放位置,稱為迭代器失效。迭代器失效的情況:

  • 當(dāng)容器調(diào)用erase()方法后,當(dāng)前位置到容器末尾元素的所有迭代器全部失效。
  • 當(dāng)容器調(diào)用insert()方法后,當(dāng)前位置到容器末尾元素的所有迭代器全部失效。
  • 如果容器擴(kuò)容,在其他地方重新又開辟了一塊內(nèi)存,原來容器底層的內(nèi)存上所保存的迭代器全都失效。

迭代器失效有三種情況,由于底層的存儲(chǔ)數(shù)據(jù)結(jié)構(gòu),分三種情況:

  • 序列式迭代器失效,序列式容器(std::vector和std::deque),其對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)分配在連續(xù)的內(nèi)存中,對(duì)其中的迭代器進(jìn)行insert和erase操作都會(huì)使得刪除點(diǎn)和插入點(diǎn)之后的元素挪位置,進(jìn)而導(dǎo)致插入點(diǎn)和刪除掉之后的迭代器全部失效??梢岳胑rase迭代器接口返回的是下一個(gè)有效的迭代器。
  • 鏈表式迭代器失效,鏈表式容器(std::list)使用鏈表進(jìn)行數(shù)據(jù)存儲(chǔ),插入或者刪除只會(huì)對(duì)當(dāng)前的節(jié)點(diǎn)造成影響,不會(huì)影響其他的迭代器??梢岳胑rase迭代器接口返回的是下一個(gè)有效的迭代器,或者將當(dāng)前的迭代器指向下一個(gè)erase(iter++)。
  • 關(guān)聯(lián)式迭代器失效,關(guān)聯(lián)式容器,如map, set,multimap,multiset等,使用紅黑樹進(jìn)行數(shù)據(jù)存儲(chǔ),刪除當(dāng)前的迭代器,僅會(huì)使當(dāng)前的迭代器失效。erase迭代器的返回值為 void(C++11之前),可以采用erase(iter++)的方式進(jìn)行刪除。值得一提的是,在最新的C++11標(biāo)準(zhǔn)中,已經(jīng)新增了一個(gè)map::erase函數(shù)執(zhí)行后會(huì)返回下一個(gè)元素的iterator,因此可以使用erase的返回值獲取下一個(gè)有效的迭代器。

在實(shí)現(xiàn)上有兩種模板,其一是通過 erase 獲得下一個(gè)有效的 iterator,使用于序列式迭代器和鏈表式迭代器(C++11開始關(guān)聯(lián)式迭代器也可以使用)

for (auto it = elements.begin(); it != elements.end(); ) {
    if (ShouldDelete(*it)) {
        it = elements.erase(it); // erase刪除元素,返回下一個(gè)迭代器
    } else {
        it++;
    }
}

其二是,遞增當(dāng)前迭代器,適用于鏈表式迭代器和關(guān)聯(lián)式迭代器。

for (auto it = elements.begin(); it != elements.end(); ) {
    if (ShouldDelete(*it)) {
        elements.erase(it++); 
    } else {
        it++;
    }
}

四、對(duì)象拷貝

在眾多編程語言中C++的優(yōu)勢之一便是其高性能,可是開發(fā)者代碼寫得不好(比如:很多不必要的對(duì)象拷貝),直接會(huì)影響到代碼性能,接下來就講幾個(gè)常見的會(huì)引起無意義拷貝的場景。

1.for循環(huán):

std::vector<std::string> vec;
for(std::string s: vec) {
}
// or
for(auto s: vec) {
}

這里每個(gè)string都會(huì)被拷貝一次,為避免無意義拷貝可以將其改成:

for(const auto& s: vec) 或者 for (const std::string& s: vec)

2.lambda捕獲

// 獲取對(duì)應(yīng)消息類型的內(nèi)容
std::string GetRichTextMessageXxxContent(const std::shared_ptr<model::Message>& message,
 const std::map<model::MessageId, std::map<model::UserId, std::string>>& related_user_names,
 const model::UserId& login_userid,
 bool for_message_index) {
 // ...
 // 解析RichText內(nèi)容
 return DecodeRichTextMessage(message, [=](uint32_t item_type, const std::string& data) {
  std::string output_text;
  // ...
  return output_text;
  });
}

上述代碼用于解析獲取文本消息內(nèi)容,涉及到富文本消息的解析和一些邏輯的計(jì)算,高頻調(diào)用,他在解析RichText內(nèi)容的callback中直接簡單粗暴的按值捕獲了所有變量,將所有變量都拷貝了一份,這里造成不必要的性能損耗,尤其上面那個(gè)std::map。這里可以改成按引用來捕獲,規(guī)避不必要的拷貝。

lambda函數(shù)在捕獲時(shí)會(huì)將被捕獲對(duì)象拷貝,如果捕獲的對(duì)象很多或者很占內(nèi)存,將會(huì)影響整體的性能,可以根據(jù)需求使用引用捕獲或者按需捕獲:

auto func = &a{};

auto func = a = std::move(a){}; (限C++14以后)

3.隱式類型轉(zhuǎn)換

std::map<int, std::string> myMap = {{1, "One"}, {2, "Two"}, {3, "Three"}};
for (const std::pair<int, std::string>& pair : myMap) {
    //...
}

這里在遍歷關(guān)聯(lián)容器時(shí),看著是const引用的,心想著不會(huì)發(fā)生拷貝,但是因?yàn)轭愋湾e(cuò)了還是會(huì)發(fā)生拷貝,std::map 中的鍵值對(duì)是以 std::pair<const Key, T> 的形式存儲(chǔ)的,其中key是常量。因此,在每次迭代時(shí),會(huì)將當(dāng)前鍵值對(duì)拷貝到臨時(shí)變量中。在處理大型容器或頻繁遍歷時(shí),這種拷貝操作可能會(huì)產(chǎn)生一些性能開銷,所以在遍歷時(shí)推薦使用const auto&,也可以使用結(jié)構(gòu)化綁定:for(const auto& [key, value]: map){} (限C++17后)

4.函數(shù)返回值優(yōu)化

RVO是Return Value Optimization的縮寫,即返回值優(yōu)化,NRVO就是具名的返回值優(yōu)化,為RVO的一個(gè)變種,此特性從C++11開始支持。為了更清晰的了解編譯器的行為,這里實(shí)現(xiàn)了構(gòu)造/析構(gòu)及拷貝構(gòu)造、賦值操作函數(shù),如下:

class Widget {
public:
 Widget() {
  std::cout << "Widget: Constructor" << std::endl;
 }
    Widget(const Widget& other) {
        name = other.name;
        std::cout << "Widget: Copy construct" << std::endl;
    }
    Widget& operator=(const Widget& other) {
        std::cout << "Widget: Assignment construct" << std::endl;
        name = other.name;
        return *this;
    }
    ~Widget() {
        std::cout << "Widget: Destructor" << std::endl;
    }
public:
    std::string name;
};

Widget GetMyWidget(int v) {
    Widget w;
    if (v % 2 == 0) {
        w.name = 1;
        return w;
    } else {
        return w;
    }
}
int main(){
    const Widget& w = GetMyWidget(2); // (1)
    Widget w = GetMyWidget(2); // (2)
    GetMyWidget(2); // (3)
    return 0;
}

運(yùn)行上面代碼,跑出的結(jié)果:

未優(yōu)化:(msvc 2022, C++14)
Widget: Constructor
Widget: Copy construct
Widget: Destructor
Widget: Destructor



優(yōu)化后:
Widget: Constructor
Widget: Destructor

針對(duì)上面(1)(2)(3)的調(diào)用,我之前也是有點(diǎn)迷惑,以為要減少拷貝必須得用常引用來接,但是發(fā)現(xiàn)編譯器進(jìn)行返回值優(yōu)化后(1)(2)(3)運(yùn)行結(jié)果都是一樣的,也就是日常開發(fā)中,針對(duì)函數(shù)中返回的臨時(shí)對(duì)象,可以用對(duì)象的常引用或者新的一個(gè)對(duì)象來接,最后的影響其實(shí)可以忽略不計(jì)的。不過個(gè)人還是傾向于對(duì)象的常引用來接,一是出于沒有優(yōu)化時(shí)(編譯器不支持或者不滿足RVO條件)可以減少一次拷貝,二是如果返回的是對(duì)象的引用時(shí)可以避免拷貝。但是也要注意不要返回臨時(shí)對(duì)象的引用。

// pb協(xié)議接口實(shí)現(xiàn)
inline const ::PB::XXXConfig& XXConfigRsp::config() const {
   //...
}
void XXSettingView::SetSettingInfo(const PB::XXConfigRsp& rsp){
 const auto config = rsp.config(); // 內(nèi)部返回的是對(duì)象的引用,這里沒有引用來接導(dǎo)致不必要的拷貝
}

當(dāng)遇到上面這種返回對(duì)象的引用時(shí),外部最好也是用對(duì)象的引用來接,減少不必要的拷貝。

此外,如果Widget的拷貝賦值操作比較耗時(shí),通常在使用函數(shù)返回這個(gè)類的一個(gè)對(duì)象時(shí)也是會(huì)有一定的講究的。

// style 1
Widget func(Args param);
// style 2
bool func(Widget* ptr, Args param);

上面的兩種方式都能達(dá)到同樣的目的,但直觀上的使用體驗(yàn)的差別也是非常明顯的:

style 1只需要一行代碼,而style 2需要兩行代碼,可能大多數(shù)人直接無腦style 1

// style 1
Widget obj = func(params);
// style 2
Widget obj;
func(&obj, params);

但是,能達(dá)到同樣的目的,消耗的成本卻未必是一樣的,這取決于多個(gè)因素,比如編譯器支持的特性、C++語言標(biāo)準(zhǔn)的規(guī)范強(qiáng)制性等等。

看起來style 2雖然需要寫兩行代碼,但函數(shù)內(nèi)部的成本卻是確定的,只會(huì)取決于你當(dāng)前的編譯器,外部即使采用不同的編譯器進(jìn)行函數(shù)調(diào)用,也并不會(huì)有多余的時(shí)間開銷和穩(wěn)定性問題。使用style 1時(shí),較復(fù)雜的函數(shù)實(shí)現(xiàn)可能并不會(huì)如你期望的使用RVO優(yōu)化,如果編譯器進(jìn)行RVO優(yōu)化,使用style 1無疑是比較好的選擇。利用好編譯器RVO特性,也是能為程序帶來一定的性能提升。

5.函數(shù)傳參使用對(duì)象的引用

effective C++中也提到了:以pass-by-reference-to-const替換pass-by-value

指在函數(shù)參數(shù)傳遞時(shí),將原本使用"pass-by-value"(按值傳遞)的方式改為使用 "pass-by-reference-to-const"(按常量引用傳遞)的方式。

在 "pass-by-value" 中,函數(shù)參數(shù)會(huì)創(chuàng)建一個(gè)副本,而在 "pass-by-reference-to-const" 中,函數(shù)參數(shù)會(huì)成為原始對(duì)象的一個(gè)引用,且為了避免修改原始對(duì)象,使用了常量引用。

通過使用 "pass-by-reference-to-const",可以避免在函數(shù)調(diào)用時(shí)進(jìn)行對(duì)象的拷貝操作,從而提高程序的性能和效率;還可以避免對(duì)象被切割問題:當(dāng)一個(gè)派生類對(duì)象以傳值的方式傳入一個(gè)函數(shù),但是該函數(shù)的形參是基類,則只會(huì)調(diào)用基類的構(gòu)造函數(shù)構(gòu)造基類部分,派生類的新特性將會(huì)被切割。此外,使用常量引用還可以確保函數(shù)內(nèi)部不會(huì)意外地修改原始對(duì)象的值。

五、std::shared_ptr線程安全

對(duì)shared_ptr相信大家都很熟悉,但是一提到是否線程安全,可能很多人心里就沒底了,借助本節(jié),對(duì)shared_ptr線程安全方面的問題進(jìn)行分析和解釋。shared_ptr的線程安全問題主要有兩種:1. 引用計(jì)數(shù)的加減操作是否線程安全; 2. shared_ptr修改指向時(shí)是否線程安全。

1.引用計(jì)數(shù)

shared_ptr中有兩個(gè)指針,一個(gè)指向所管理數(shù)據(jù)的地址,另一個(gè)指向執(zhí)行控制塊的地址。

執(zhí)行控制塊包括對(duì)關(guān)聯(lián)資源的引用計(jì)數(shù)以及弱引用計(jì)數(shù)等。在前面我們提到shared_ptr支持跨線程操作,引用計(jì)數(shù)變量是存儲(chǔ)在堆上的,那么在多線程的情況下,指向同一數(shù)據(jù)的多個(gè)shared_ptr在進(jìn)行計(jì)數(shù)的++或--時(shí)是否線程安全呢?

引用計(jì)數(shù)在STL中的定義如下:

_Atomic_word _M_use_count;   // #shared
_Atomic_word _M_weak_count;  // #weak + (#shared != 0)

當(dāng)對(duì)shared_ptr進(jìn)行拷貝時(shí),引入計(jì)數(shù)增加,實(shí)現(xiàn)如下:

template <>
inline bool _Sp_counted_base<_S_atomic>::_M_add_ref_lock_nothrow() noexcept {
    // Perform lock-free add-if-not-zero operation.
    _Atomic_word __count = _M_get_use_count();
    do {
        if (__count == 0) return false;
        // Replace the current counter value with the old value + 1, as
        // long as it's not changed meanwhile.
    } while (!__atomic_compare_exchange_n(&_M_use_count, &__count, __count + 1, true, __ATOMIC_ACQ_REL,
                                          __ATOMIC_RELAXED));
    return true;
}

template <>
inline void _Sp_counted_base<_S_single>::_M_add_ref_copy() {
    ++_M_use_count;
}

對(duì)引用計(jì)數(shù)的增加主要有以下2種方法:_M_add_ref_copy函數(shù),對(duì)_M_use_count + 1,是原子操作。_M_add_ref_lock函數(shù),是調(diào)用__atomic_compare_exchange_n``實(shí)現(xiàn)的``,主要邏輯仍然是_M_use_count + 1,而該函數(shù)是線程安全的,和_M_add_ref_copy的區(qū)別是對(duì)不同_Lock_policy有不同的實(shí)現(xiàn),包含直接加、原子操作加、加鎖。

因此我們可以得出結(jié)論:在多線程環(huán)境下,管理同一個(gè)數(shù)據(jù)的shared_ptr在進(jìn)行計(jì)數(shù)的增加或減少的時(shí)候是線程安全的,這是一波原子操作。

2.修改指向

修改指向分為操作同一個(gè)shared_ptr對(duì)象和操作不同的shared_ptr對(duì)象兩種。

(1) 多線程代碼操作的是同一個(gè)shared_ptr的對(duì)象

比如std::thread的回調(diào)函數(shù),是一個(gè)lambda表達(dá)式,其中引用捕獲了一個(gè)shared_ptr對(duì)象

shared_ptr<A> sp1 = make_shared<A>();
std::thread td([&sp1] () {....});

又或者通過回調(diào)函數(shù)的參數(shù)傳入的shared_ptr對(duì)象,參數(shù)類型是指針或引用:

`指針類型:void fn(shared_ptr<A>* sp) { ... }std::thread td(fn, &sp1);引用類型:void fn(shared_ptr<A>& sp) { ... }std::thread td(fn, std::ref(sp1));`

當(dāng)你在多線程回調(diào)中修改shared_ptr指向的時(shí)候,這時(shí)候確實(shí)不是線程安全的。

void fn(shared_ptr<A>& sp) {
    if (..) {
        sp = other_sp;
    } else if (...) {
        sp = other_sp2;
    }
}

shared _ptr內(nèi)數(shù)據(jù)指針要修改指向,sp原先指向的引用計(jì)數(shù)的值要減去1,other_sp指向的引用計(jì)數(shù)值要加1。然而這幾步操作加起來并不是一個(gè)原子操作,如果多個(gè)線程都在修改sp的指向的時(shí)候,那么有可能會(huì)出問題。比如在導(dǎo)致計(jì)數(shù)在操作-1的時(shí)候,其內(nèi)部的指向已經(jīng)被其他線程修改過了,引用計(jì)數(shù)的異常會(huì)導(dǎo)致某個(gè)管理的對(duì)象被提前析構(gòu),后續(xù)在使用到該數(shù)據(jù)的時(shí)候觸發(fā)coredump。當(dāng)然如果你沒有修改指向的時(shí)候,是沒有問題的。也就是:

  • 同一個(gè)shared_ptr對(duì)象被多個(gè)線程同時(shí)讀是安全的
  • 同一個(gè)shared_ptr對(duì)象被多個(gè)線程同時(shí)讀寫是不安全的

(2) 多線程代碼操作的不是同一個(gè)shared_ptr的對(duì)象

這里指的是管理的數(shù)據(jù)是同一份,而shared_ptr不是同一個(gè)對(duì)象,比如多線程回調(diào)的lambda是按值捕獲的對(duì)象。

std::thread td([sp1] () {....});

或者參數(shù)傳遞的shared_ptr是值傳遞,而非引用:

void fn(shared_ptr<A> sp) {
    ...
}
std::thread td(fn, sp1);

這時(shí)候每個(gè)線程內(nèi)看到的sp,他們所管理的是同一份數(shù)據(jù),用的是同一個(gè)引用計(jì)數(shù)。但是各自是不同的對(duì)象,當(dāng)發(fā)生多線程中修改sp指向的操作的時(shí)候,是不會(huì)出現(xiàn)非預(yù)期的異常行為的。也就是說,如下操作是安全的:

void fn(shared_ptr<A> sp) {
    if (..) {
        sp = other_sp;
    } else if (...) {
        sp = other_sp2;
    }
}

盡管前面我們提到了如果是按值捕獲(或傳參)的shared_ptr對(duì)象,那么該對(duì)象是線程安全的,然而話雖如此,但卻可能讓人誤入歧途。因?yàn)槲覀兪褂胹hared_ptr更多的是操作其中的數(shù)據(jù),對(duì)齊管理的數(shù)據(jù)進(jìn)行讀寫,盡管在按值捕獲的時(shí)候shared_ptr是線程安全的,我們不需要對(duì)此施加額外的同步操作(比如加解鎖),但是這并不意味著shared_ptr所管理的對(duì)象是線程安全的!請(qǐng)注意這是兩回事。

最后再來看下std官方手冊(cè)是怎么講的:

All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object. If multiple threads of execution access the same instance of shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.

這段話的意思是,shared_ptr 的所有成員函數(shù)(包括復(fù)制構(gòu)造函數(shù)和復(fù)制賦值運(yùn)算符)都可以由多個(gè)線程在不同的 shared_ptr 實(shí)例上調(diào)用,即使這些實(shí)例是副本并且共享同一個(gè)對(duì)象的所有權(quán)。如果多個(gè)執(zhí)行線程在沒有同步的情況下訪問同一個(gè) shared_ptr 實(shí)例,并且這些訪問中的任何一個(gè)使用了 shared_ptr 的非 const 成員函數(shù),則會(huì)發(fā)生數(shù)據(jù)競爭;可以使用shared_ptr的原子函數(shù)重載來防止數(shù)據(jù)競爭。

我們可以得到下面的結(jié)論:

(1) 多線程環(huán)境中,對(duì)于持有相同裸指針的std::shared_ptr實(shí)例,所有成員函數(shù)的調(diào)用都是線程安全的。

  • 當(dāng)然,對(duì)于不同的裸指針的 std::shared_ptr 實(shí)例,更是線程安全的
  • 這里的 “成員函數(shù)” 指的是 std::shared_ptr 的成員函數(shù),比如 get ()、reset ()、operrator->()等

(2) 多線程環(huán)境中,對(duì)于同一個(gè)std::shared_ptr實(shí)例,只有訪問const的成員函數(shù),才是線程安全的,對(duì)于非const成員函數(shù),是非線程安全的,需要加鎖訪問。

首先來看一下 std::shared_ptr 的所有成員函數(shù),只有前3個(gè)是 non-const 的,剩余的全是 const 的:

成員函數(shù)

是否const

operator=

non-const

reset

non-const

swap

non-const

get

const

operator、operator->

const

operator

const

use_count

const

operator bool

const

unique

const

講了這么多,來個(gè)栗子實(shí)踐下:

ant::Promise<JsAPIResultCode, CefRefPtr<CefDictionaryValue>>
XXXHandler::OnOpenSelectContactH5(const JsAPIContext& context, std::shared_ptr<RequestType> arguments) {
 ant::Promise<JsAPIResultCode, CefRefPtr<CefDictionaryValue>> promise;
 base::GetUIThread()->PostTask(weak_lambda(this, [this, promise, context, arguments]() {

  auto b_executed_flag = std::make_shared<std::atomic_bool>(false);
  auto ext_param = xx::OpenWebViewWindow::OpenURLExtParam();
  // ...
  // SelectCorpGroupContact jsapi的回調(diào)
  ext_param.select_group_contact_callback = [promise, b_executed_flag](
   JsAPIResultCode resCode, CefRefPtr<CefDictionaryValue> res) mutable {
    *b_executed_flag = true;
    base::GetUIThread()->PostTask([promise, resCode, res]() {
     promise.resolve(resCode, res);
     });
  };
  // 窗口關(guān)閉回調(diào)
  ext_param.dismiss_callback = [promise, b_executed_flag]() {
   if (*b_executed_flag) {
    return;
   }
   promise.resolve(JSAPI_RESULT_CANCEL, CefDictionaryValue::Create());
  };
  // ...
  xx::OpenWebViewWindow::OpenURL(nullptr, url, false, ext_param);
 }));
 return promise;
}

該段代碼場景是一個(gè)Jsapi接口,在接口中打開另一個(gè)webview的選人窗口,選人窗口操作后或者關(guān)閉時(shí)都需要回調(diào)下,將結(jié)果返回jsapi。選人完畢確認(rèn)后會(huì)回調(diào)select_group_contact_callback,同時(shí)關(guān)閉webview窗口還會(huì)回調(diào)dismiss_callback,這倆回調(diào)里面都會(huì)回包,這里還涉及多線程調(diào)用。這倆回調(diào)只能調(diào)用一個(gè),為了能簡單達(dá)到這種效果,作者用std::shared_ptrstd::atomic_bool b_executed_flag來處理多線程同步,如果一個(gè)回調(diào)已執(zhí)行就標(biāo)記下,shared_ptr本身對(duì)引用計(jì)數(shù)的操作是線程安全的,通過原子變量std::atomic_bool來保證其管理的對(duì)象的線程安全。

六、std::map

// 定義數(shù)據(jù)緩存類
class DataCache {
private:
    std::map<std::string, std::string> cache;
public:
    void addData(const std::string& key, const std::string& value) {
        cache[key] = value;
    }
    std::string getData(const std::string& key) {
        return cache[key];
    }
};

在上述示例中,簡單定義了個(gè)數(shù)據(jù)緩存類,使用 std::map作為數(shù)據(jù)緩存,然后提供addData添加數(shù)據(jù)到緩存,getData從map緩存中獲取數(shù)據(jù)。一切看起來毫無違和感,代碼跑起來也沒什么問題,但是如果使用沒有緩存的key去getData, 發(fā)現(xiàn)會(huì)往緩存里面新插入一條value為默認(rèn)值的記錄。

需要注意的是,如果我們使用 [] 運(yùn)算符訪問一個(gè)不存在的鍵,并且在插入新鍵值對(duì)時(shí)沒有指定默認(rèn)值,那么新鍵值對(duì)的值將是未定義的。因此,在使用 [] 運(yùn)算符訪問 std::map 中的元素時(shí),應(yīng)該始終確保該鍵已經(jīng)存在或者在插入新鍵值對(duì)時(shí)指定了默認(rèn)值。

void addData(const std::string& key, const std::string& value) {
    if(key.empty()) return;
    cache[key] = value;
}
std::string getData(const std::string& key) {
    const auto iter = cache.find(key);
    return iter != cache.end() ? iter->second : "";
}

七、sizeof & strlen

相信大家都有過這樣的經(jīng)歷,在項(xiàng)目中使用系統(tǒng)API或者與某些公共庫編寫邏輯時(shí),需要C++與C 字符串混寫甚至轉(zhuǎn)換,在處理字符串結(jié)構(gòu)體的時(shí)候就免不了使用sizeof和strlen,這倆看著都有計(jì)算size的能力,有時(shí)候很容易搞混淆或者出錯(cuò)。

  • sizeof 是個(gè)操作符,可用于任何類型或變量,包括數(shù)組、結(jié)構(gòu)體、指針等, 返回的是一個(gè)類型或變量所占用的字節(jié)數(shù); 在編譯時(shí)求值,不會(huì)對(duì)表達(dá)式進(jìn)行求值。
  • strlen 是個(gè)函數(shù),只能用于以 null 字符結(jié)尾的字符串,返回的是一個(gè)以 null 字符('\0')結(jié)尾的字符串的長度(不包括 null 字符本身),且在運(yùn)行時(shí)才會(huì)計(jì)算字符串的長度。

需要注意的是,使用 sizeof 操作符計(jì)算數(shù)組長度時(shí)需要注意數(shù)組元素類型的大小。例如,對(duì)于一個(gè) int 類型的數(shù)組,使用 sizeof 操作符計(jì)算其長度應(yīng)該為 sizeof(array) / sizeof(int)。而對(duì)于一個(gè)字符數(shù)組,使用strlen函數(shù)計(jì)算其長度應(yīng)該為 strlen(array)。

char str[] = "hello";
char *p = str;

此時(shí),用sizeof(str)得到的是6,因?yàn)閔ello是5個(gè)字符,系統(tǒng)儲(chǔ)存的時(shí)候會(huì)在hello的末尾加上結(jié)束標(biāo)識(shí)\0,一共為6個(gè)字符;

而sizeof(p)得到的卻是4,它求得的是指針變量p的長度,在32位機(jī)器上,一個(gè)地址都是32位,即4個(gè)字節(jié)。

  • 用sizeof(p)得到的是1,因?yàn)閜定義為char,相當(dāng)于一個(gè)字符,所以只占一個(gè)字節(jié)。
  • 用strlen(str),得到的會(huì)是5,因?yàn)閟trlen求得的長度不包括最后的\0。
  • 用strlen(p),得到的是5,與strlen(str)等價(jià)。

上面的是sizeof和strlen的區(qū)別,也是指針字符串和數(shù)組字符串的區(qū)別。

const char* src = "hello world";
char* dest = NULL;
int len = strlen(src); // 這里很容易出錯(cuò),寫成sizeof(src)就是求指針的長度,即4
dest = (char*)malloc(len + 1); // 這里很容易出錯(cuò),寫成len
char* d = dest;
const char* s = &src[len - 1]; // 這里很容易出錯(cuò),寫成len
while (len-- != 0) {
     *d++ = *s--;
}
*d = '\0'; // 這句很容易漏寫
printf("%sIn", dest);
free(dest);

八、std::async真的異步嗎?

std::async是C++11開始支持多線程時(shí)加入的同步多線程構(gòu)造函數(shù),其彌補(bǔ)了std::thread沒有返回值的問題,并加入了更多的特性,使得多線程更加靈活。

顧名思義,std::async是一個(gè)函數(shù)模板,它將函數(shù)或函數(shù)對(duì)象作為參數(shù)(稱為回調(diào))并異步運(yùn)行它們,最終返回一個(gè)std::future,它存儲(chǔ)std::async()執(zhí)行的函數(shù)對(duì)象返回的值,為了從中獲取值,程序員需要調(diào)用其成員 future::get.

那std::async一定是異步執(zhí)行嗎?先來看段代碼:

int calculate_sum(const std::vector<int>& numbers) {
    std::cout << "Start Calculate..." << std::endl; // (4)
    int sum = 0;
    for (int num : numbers) {
        sum += num;
    }
    return sum;
}
int main() {
    std::vector<int> numbers = { 88, 101, 56, 203, 72, 135 };
    std::future<int> future_sum = std::async(calculate_sum, numbers);
    std::cout << "Other operations are in progress..." << std::endl; // (1)
    int counter = 1;
    while (counter <= 1000000000) {
        counter++;
    }
    std::cout << "Other operations are completed." << std::endl; // (2)
    // 等待異步任務(wù)完成并獲取結(jié)果
    int sum = future_sum.get();
    std::cout << "The calculation result is:" << sum << std::endl; // (3)
    return 0;
}

直接運(yùn)行上面的代碼,輸出結(jié)果如下:

Other operations are in progress...
Start Calculate...
Other operations are completed.
The calculation result is:655

執(zhí)行完(1) 就去執(zhí)行(4), 然后再(2)(3),說明這里是異步執(zhí)行的。那可以認(rèn)為async一定是異步的嗎?

如果改成std::async(std::launch::deferred, calculate_sum, numbers); 運(yùn)行結(jié)果如下:

Other operations are in progress...
Other operations are completed.
Start Calculate...
The calculation result is:655

執(zhí)行完(1) (2), 然后再(4)(3), 說明是真正調(diào)用std::future<>::get()才去執(zhí)行的,如果沒有調(diào)用get,那么就一直不會(huì)執(zhí)行。

std::async是否異步受參數(shù)控制的,其第一個(gè)參數(shù)是啟動(dòng)策略,它控制 std::async 的異步行為??梢允褂?3 種不同的啟動(dòng)策略創(chuàng)建 std::async ,即:

  • std::launch::async 它保證異步行為,即傳遞的函數(shù)將在單獨(dú)的線程中執(zhí)行
  • std::launch::deferred 非異步行為,即當(dāng)其他線程將來調(diào)用get()來訪問共享狀態(tài)時(shí),將調(diào)用函數(shù)
  • std::launch::async | std::launch::deferred 它是默認(rèn)行為。使用此啟動(dòng)策略,它可以異步運(yùn)行或不異步運(yùn)行,具體取決于系統(tǒng)上的負(fù)載,但我們無法控制它

如果我們不指定啟動(dòng)策略,其行為類似于std::launch::async | std::launch::deferred. 也就是不一定是異步的。

Effective Modern C++ 里面也提到了,如果異步執(zhí)行是必須的,則指定std::launch::async策略。

九、內(nèi)存泄漏?

對(duì)于這樣的一個(gè)函數(shù):

void processwidget(std::shared_ptrpw, int);

如果使用以下方式調(diào)用,會(huì)有什么問題嗎?

processwidget(std::shared_ptr(new Widget), priority());

一眼看上去覺得沒啥問題,甚至可能新手C++開發(fā)者也會(huì)這么寫,其實(shí)上面調(diào)用可能會(huì)存在內(nèi)存泄漏。

編譯器在生成對(duì)processWidget函數(shù)的調(diào)用之前,必須先解析其中的參數(shù)。processWidget函數(shù)接收兩個(gè)參數(shù),分別是智能指針的構(gòu)造函數(shù)和整型的函數(shù)priority()。在調(diào)用智能指針構(gòu)造函數(shù)之前,編譯器必須先解析其中的new Widget語句。因此,解析該函數(shù)的參數(shù)分為三步:

(1) 調(diào)用priority();

(2) 執(zhí)行new Widget.

(3) 調(diào)用std:shared_ptr構(gòu)造函數(shù)

C++編譯器以什么樣的固定順序去完成上面的這些事情是未知的,不同的編譯器是有差異的。在C++中可以確定(2)一定先于(3)執(zhí)行,因?yàn)閚ew Widoet還要被傳遞作為std::shared_ptr構(gòu)造函數(shù)的一個(gè)實(shí)參。然而,對(duì)于priority()的調(diào)用可以在第(1)、(2)、(3)步執(zhí)行,假設(shè)編譯器選擇以(2)執(zhí)行它,最終的操作次序如下: (1) 執(zhí)行new Widget; (2) 調(diào)用priority(): (3)調(diào)用std::shared_ptr構(gòu)造函數(shù)。但是,如果priority()函數(shù)拋出了異常,經(jīng)由new Widget返回的指針尚未被智能指針管理,將會(huì)遺失導(dǎo)致內(nèi)存泄漏。

解決方法: 使用一個(gè)單獨(dú)的語句來創(chuàng)建智能指針對(duì)象。

std::shared ptr<Widget> pw(new widget); // 放在單獨(dú)的語句中
processwidget(pw, priority()):
// or
processwidget(std::make_shared<Widget>(), priority());

編譯器是逐語句編譯的,通過使用一個(gè)單獨(dú)的語句來構(gòu)造智能指針對(duì)象,編譯器就不會(huì)隨意改動(dòng)解析順序,保證了生成的機(jī)器代碼順序是異常安全的。

總結(jié):尤其是在跨平臺(tái)開發(fā)的時(shí)候更加要注意這類隱晦的異常問題,Effective C++中也提到了,要以獨(dú)立語句將new對(duì)象存儲(chǔ)于智能指針內(nèi)。如果不這樣做,一旦異常被拋出,有可能導(dǎo)致難以察覺的內(nèi)存泄漏。

十、const/constexpr

如果C++11中引入的新詞要評(píng)一個(gè)"最令人困惑"獎(jiǎng),那么constexprhen很有可能獲此殊榮。當(dāng)它應(yīng)用于對(duì)象時(shí),其實(shí)就是一個(gè)加強(qiáng)版的const,但應(yīng)用于函數(shù)時(shí),卻有著相當(dāng)不同的意義。在使用 C++ const和consterpx的時(shí)候,可能都會(huì)犯暈乎,那constexpr和 const都有什么區(qū)別,這節(jié)簡單梳理下。

1.const

const一般的用法就是修飾變量、引用、指針,修飾之后它們就變成了常量,需要注意的是const并未區(qū)分出編譯期常量和運(yùn)行期常量,并且const只保證了運(yùn)行時(shí)不直接被修改。

一般的情況,const 也就簡單這么用一下,const 放在左邊,表示常量:

  • const int x = 100; // 常量
  • const int& rx = x; // 常量引用
  • const int* px = &x; // 常量指針

給變量加上const之后就成了“常量”,只能讀、不能修改,編譯器會(huì)檢查出所有對(duì)它的修改操作,發(fā)出警告,在編譯階段防止有意或者無意的修改。這樣一來,const常量用起來就相對(duì)安全一點(diǎn)。在設(shè)計(jì)函數(shù)的時(shí)候,將參數(shù)用 const 修飾的話,可以保證效率和安全。

除此之外,const 還能聲明在成員函數(shù)上,const 被放在了函數(shù)的后面,表示這個(gè)函數(shù)是一個(gè)“常量”,函數(shù)的執(zhí)行過程是 const 的,不會(huì)修改成員變量。

此外,const還有下面這種與指針結(jié)合的比較繞的用法:

int a = 1;
const int b = 2;
const int* p = &a;
int const* p1 = &a;

// *p = 2; // error C3892: “p”: 不能給常量賦值
p = &b;
// *p1 = 3; // error C3892: “p1”: 不能給常量賦值
p1 = &b;

int* const p2 = &a;
//p2 = &b; // error C2440: “=”: 無法從“const int *”轉(zhuǎn)換為“int *const ”
*p2 = 5;
const int* const p3 = &a;

const int 與 int const并無很大區(qū)別,都表示: 指向常量的指針,可以修改指針本身,但不能通過指針修改所指向的值。

而對(duì)于int *const,則是表示:一個(gè)常量指針,可以修改所指向的值,但不能修改指針本身。

const int* const 表示一個(gè)不可修改的指針,既不能修改指針本身,也不能通過指針修改所指向的值。

總之,const默認(rèn)與其左邊結(jié)合,當(dāng)左邊沒有任何東西則與右邊結(jié)合。

2.constexpr

表面上看,constexpr不僅是const,而且在編譯期間就已知,這種說法并不全面,當(dāng)它應(yīng)用在函數(shù)上時(shí),就跟它名字有點(diǎn)不一樣了。使用constexpr關(guān)鍵字可以將對(duì)象或函數(shù)定義為在編譯期間可求值的常量,這樣可以在編譯期間進(jìn)行計(jì)算,避免了運(yùn)行時(shí)的開銷。

constexpr對(duì)象 必須在編譯時(shí)就能確定其值,并且通常用于基本數(shù)據(jù)類型。例如:

  • constexpr int MAX_SIZE = 100; // 定義一個(gè)編譯時(shí)整型常量
  • constexpr double PI = 3.14159; // 定義一個(gè)編譯時(shí)雙精度浮點(diǎn)型常量

const和constexpr變量之間的主要區(qū)別在于變量的初始化,const可以推遲到運(yùn)行時(shí),constexpr變量必須在編譯時(shí)初始化。const 并未區(qū)分出編譯期常量和運(yùn)行期常量,并且const只保證了運(yùn)行時(shí)不直接被修改,而constexpr是限定在了編譯期常量。簡而言之,所有constexpr對(duì)象都是const對(duì)象,而并非所有的const對(duì)象都是constexpr對(duì)象。

  • 當(dāng)變量具有字面型別(literal type)(這樣的型別能夠持有編譯期可以決議的值)并已初始化時(shí),可以使用constexpr來聲明該變量。如果初始化由構(gòu)造函數(shù)執(zhí)行,則必須將構(gòu)造函數(shù)聲明為constexpr.
  • 當(dāng)滿足這兩個(gè)條件時(shí),可以聲明引用constexpr:引用的對(duì)象由常量表達(dá)式初始化,并且在初始化期間調(diào)用的任何隱式轉(zhuǎn)換也是常量表達(dá)式。

constexpr變量或函數(shù)的所有聲明都必須具有constexpr說明符。

constexpr float x = 42.0;
constexpr float y{108};
constexpr float z = exp(5, 3);
constexpr int i; // Error! Not initialized
int j = 0;
constexpr int k = j + 1; //Error! j not a constant expression

constexpr函數(shù) 是指能夠在編譯期間計(jì)算結(jié)果的函數(shù)。它們的參數(shù)和返回值類型必須是字面值類型,并且函數(shù)體必須由單個(gè)返回語句組成。例如:

constexpr int square(int x) {
 return x * x;
 }

constexpr int result = square(5); // 在編譯期間計(jì)算結(jié)果,result 的值為 25

使用 constexpr 可以提高程序的性能和效率,因?yàn)樗试S在編譯期間進(jìn)行計(jì)算,避免了運(yùn)行時(shí)的計(jì)算開銷。同時(shí),constexpr 還可以用于指定數(shù)組的大小、模板參數(shù)等場景,提供更靈活的編程方式。

對(duì)constexpr函數(shù)的理解:

  • constexpr函數(shù)可以用在要求編譯器常量的語境中。在這樣的語境中,如果你傳給constexpr函數(shù)的實(shí)參值是在編譯期已知的,則結(jié)果也會(huì)在編譯期間計(jì)算出來。如果任何一個(gè)實(shí)參值在編譯期間未知,則代碼將無法通過編譯。
  • 在調(diào)用constexpr函數(shù)時(shí),若傳入的值有一個(gè)或多個(gè)在編譯期間未知,則它的運(yùn)作方式和普通函數(shù)無異,也就是它也是在運(yùn)行期執(zhí)行結(jié)果的計(jì)算。也就是說,如果一個(gè)函數(shù)執(zhí)行的是同樣的操作,僅僅應(yīng)用語境一個(gè)是要求編譯期常量,一個(gè)是用于所有其他值的話,那就不必寫兩個(gè)函數(shù)。constexpr函數(shù)就可以同時(shí)滿足需求。
constexpr int square(int x) {
 return x * x;
 }

比起非constexpr對(duì)象或constexpr函數(shù)而言,constexpr對(duì)象或是constexpr函數(shù)可以用在一個(gè)作用域更廣的語境中。只要有可能使用constexpr,就使用它吧。


責(zé)任編輯:趙寧寧 來源: 騰訊技術(shù)工程
相關(guān)推薦

2023-11-01 15:32:58

2024-04-24 13:45:00

2021-02-26 00:46:11

CIO數(shù)據(jù)決策數(shù)字化轉(zhuǎn)型

2021-02-22 17:00:31

Service Mes微服務(wù)開發(fā)

2021-05-08 12:30:03

Pythonexe代碼

2023-05-24 10:06:42

多云實(shí)踐避坑

2021-05-07 21:53:44

Python 程序pyinstaller

2022-03-04 18:11:16

信服云

2021-04-28 09:26:25

公有云DTS工具

2020-12-16 10:00:59

Serverless數(shù)字化云原生

2025-02-19 08:20:00

編程指針C++

2018-01-20 20:46:33

2022-01-23 14:29:25

C語言編程語言

2024-12-31 15:52:43

2020-06-12 11:03:22

Python開發(fā)工具

2019-02-12 15:07:42

屏幕參數(shù)PC

2018-03-26 11:14:13

程序猿bug代碼

2019-04-24 17:45:24

微服務(wù)容器青云

2025-03-26 02:00:00

API工具開發(fā)

2019-10-17 09:58:01

深度學(xué)習(xí)編程人工智能
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)