C++ 11 引入的 auto
也許是 C++ 11 最知名的特性之一, C++ 14 對 auto
和 decltype
進行了強化. 大家經常會在我的文章中看到所謂
auto value {};
這樣的宣告. 最近, 我發現了這樣的宣告有一些隱含的錯誤點. 首先, 我們來看一段程式碼 :
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
auto value {0};
cout << value << endl; //0
}
如果你經常看我的文章, 那麼上述程式碼你肯定非常熟悉, 結果如你所願就是 0
. 但是如果添加一個指派運算子, 結果可能就完全不同 :
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
auto value = {0};
cout << value << endl;
}
這是 Clang 所給定的編碼錯誤 :
Untitled.cpp:7:10: error: invalid operands to binary expression ('std::__1::ostream' (aka 'basic_ostream<char>') and 'std::initializer_list<int>')
cout << value << endl;
~~~~ ^ ~~~~~
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:218:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'const void *' for 1st argument; take the address of the argument with &
basic_ostream& operator<<(const void* __p);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/type_traits:4830:3: note: candidate function template not viable: no known conversion from 'std::__1::ostream' (aka 'basic_ostream<char>') to 'std::byte' for 1st argument
operator<< (byte __lhs, _Integer __shift) noexcept
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:194:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'std::__1::basic_ostream<char> &(*)(std::__1::basic_ostream<char> &)' for 1st argument
basic_ostream& operator<<(basic_ostream& (*__pf)(basic_ostream&))
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:198:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'basic_ios<std::__1::basic_ostream<char, std::__1::char_traits<char> >::char_type, std::__1::basic_ostream<char, std::__1::char_traits<char> >::traits_type> &(*)(basic_ios<std::__1::basic_ostream<char, std::__1::char_traits<char> >::char_type, std::__1::basic_ostream<char, std::__1::char_traits<char> >::traits_type> &)' (aka 'basic_ios<char, std::__1::char_traits<char> > &(*)(basic_ios<char, std::__1::char_traits<char> > &)') for 1st argument
basic_ostream& operator<<(basic_ios<char_type, traits_type>&
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:203:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'std::__1::ios_base &(*)(std::__1::ios_base &)' for 1st argument
basic_ostream& operator<<(ios_base& (*__pf)(ios_base&))
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:206:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'bool' for 1st argument
basic_ostream& operator<<(bool __n);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:207:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'short' for 1st argument
basic_ostream& operator<<(short __n);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:208:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'unsigned short' for 1st argument
basic_ostream& operator<<(unsigned short __n);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:209:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'int' for 1st argument
basic_ostream& operator<<(int __n);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:210:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'unsigned int' for 1st argument
basic_ostream& operator<<(unsigned int __n);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:211:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'long' for 1st argument
basic_ostream& operator<<(long __n);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:212:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'unsigned long' for 1st argument
basic_ostream& operator<<(unsigned long __n);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:213:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'long long' for 1st argument
basic_ostream& operator<<(long long __n);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:214:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'unsigned long long' for 1st argument
basic_ostream& operator<<(unsigned long long __n);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:215:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'float' for 1st argument
basic_ostream& operator<<(float __f);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:216:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'double' for 1st argument
basic_ostream& operator<<(double __f);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:217:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'long double' for 1st argument
basic_ostream& operator<<(long double __f);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:219:20: note: candidate function not viable: no known conversion from 'std::initializer_list<int>' to 'basic_streambuf<std::__1::basic_ostream<char, std::__1::char_traits<char> >::char_type, std::__1::basic_ostream<char, std::__1::char_traits<char> >::traits_type> *' (aka 'basic_streambuf<char, std::__1::char_traits<char> > *') for 1st argument
basic_ostream& operator<<(basic_streambuf<char_type, traits_type>* __sb);
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:755:1: note: candidate function template not viable: no known conversion from 'std::initializer_list<int>' to 'char' for 2nd argument
operator<<(basic_ostream<_CharT, _Traits>& __os, char __cn)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:788:1: note: candidate function template not viable: no known conversion from 'std::initializer_list<int>' to 'char' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, char __c)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:795:1: note: candidate function template not viable: no known conversion from 'std::initializer_list<int>' to 'signed char' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, signed char __c)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:802:1: note: candidate function template not viable: no known conversion from 'std::initializer_list<int>' to 'unsigned char' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, unsigned char __c)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:816:1: note: candidate function template not viable: no known conversion from 'std::initializer_list<int>' to 'const char *' for 2nd argument
operator<<(basic_ostream<_CharT, _Traits>& __os, const char* __strn)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:862:1: note: candidate function template not viable: no known conversion from 'std::initializer_list<int>' to 'const char *' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, const char* __str)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:869:1: note: candidate function template not viable: no known conversion from 'std::initializer_list<int>' to 'const signed char *' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, const signed char* __str)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:877:1: note: candidate function template not viable: no known conversion from 'std::initializer_list<int>' to 'const unsigned char *' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, const unsigned char* __str)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:1061:1: note: candidate function template not viable: no known conversion from 'std::initializer_list<int>' to 'const std::__1::error_code' for 2nd argument
operator<<(basic_ostream<_CharT, _Traits>& __os, const error_code& __ec)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:748:1: note: candidate template ignored: deduced conflicting types for parameter '_CharT' ('char' vs. 'std::initializer_list<int>')
operator<<(basic_ostream<_CharT, _Traits>& __os, _CharT __c)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:809:1: note: candidate template ignored: could not match 'const _CharT *' against 'std::initializer_list<int>'
operator<<(basic_ostream<_CharT, _Traits>& __os, const _CharT* __str)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:1044:1: note: candidate template ignored: could not match 'basic_string' against 'initializer_list'
operator<<(basic_ostream<_CharT, _Traits>& __os,
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:1069:1: note: candidate template ignored: could not match 'shared_ptr' against 'initializer_list'
operator<<(basic_ostream<_CharT, _Traits>& __os, shared_ptr<_Yp> const& __p)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:1088:1: note: candidate template ignored: could not match 'bitset' against 'initializer_list'
operator<<(basic_ostream<_CharT, _Traits>& __os, const bitset<_Size>& __x)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:1034:1: note: candidate template ignored: requirement '!is_lvalue_reference<basic_ostream<char> &>::value' was not satisfied [with _Stream = std::__1::basic_ostream<char> &, _Tp = std::initializer_list<int>]
operator<<(_Stream&& __os, const _Tp& __x)
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:1052:1: note: candidate template ignored: could not match 'basic_string_view' against 'initializer_list'
operator<<(basic_ostream<_CharT, _Traits>& __os,
^
/Library/Developer/CommandLineTools/usr/include/c++/v1/ostream:1081:1: note: candidate template ignored: could not match 'unique_ptr' against 'initializer_list'
operator<<(basic_ostream<_CharT, _Traits>& __os, unique_ptr<_Yp, _Dp> const& __p)
^
1 error generated.
從編碼錯誤中, 我們大致可以看出, value
的型別被推導為 std::initializer_list<int>
. 也許你還記得我在《C++ 學習筆記》中提到我更推薦使用類似於
T value {};
的初始化方法. 也許當時是我的運氣比較好, 我至今都沒有發現我任何文章或著其它作品在這上面栽過跟頭. 針對初始化列表式的指派式初始化方法, 如果變數需要被推導型別, 那麼它會被推導為 std::initializer_list<T>
型別, 這是 C++ 的規定
那麼我在《C++ 學習筆記》中提到的是否還是正確的? 為什麼我最開始的宣告在使用的時候沒有什麼問題, 也就是變數的型別被正確推導, 而不是被推導為 std::initializer_list<T>
型別? 這裡有兩個例外 :
- 如果變數明確被某一型別標識, 不需要編碼器來推導型別, 那麼大括號初始化預設和普通的初始化沒有什麼差別, 不論有沒有指派運算子
- 即時變數的型別需要推導, 但是如果大括號中有且唯有一個引數, 在不存在指派運算子的情況下, 編碼器就會講變數推導為引數所相關的型別 (
auto
在推導的過程中可能會忽略const
、volatile
以及參考)
這一點其實讓我覺得比較疑惑, 因為帶有指派運算子的初始化和不帶有指派運算子的初始化本身行為應該要一致. 而《Effective Modern C++》這本書中提到, 即時是不帶指派運算子的初始化, 只要使用大括號, 編碼器就會將型別推導為 std::initializer_list<T>
. 這點和編碼器的行為不太一致, 到目前為止, 三大編碼器 Clang、GCC 和 MSVC 在這一點上有共同的行為 : 帶有指派運算子的會被推導為 std::initializer_list<T>
; 不帶有指派運算子的如果大括號中有且唯有一個引數, 那麼推導為這個引數的相關型別, 如果帶有多個引數, 那麼擲出編碼錯誤. 這裡, 我們以編碼器的行為為主, 不以書中的內容為主 (可能書的作者本身只是知道這種初始化方式, 但是沒有實驗)
2020 年 7 月 25 日修正 : auto 的推導參考文章《【C++ 17 Paper 導讀】初始化列表下的 auto 推斷 – 解答我自己之前的疑問》
除了 C++ 11 的 auto
之外, C++ 14 允許編碼器來推導函式的回傳型別. 在 C++ 11 中, 你需要這樣寫 :
template <typename T>
auto index(T *arr, int n) -> decltype(arr[n]) {
return arr[n];
}
在 C++ 14 中, 可以直接省略尾置回傳型別 :
template <typename T>
auto index(T *arr, int n) {
return arr[n];
}
但是這個函式的行為可能和之前的函式不相同. 我們知道, 對一個指標使用陣列注標運算子, 起回傳值的型別是一個參考, 這也就允許我們寫出這樣的陳述式 :
arr[n] = value;
也就是說, 我們期望函式 index
可以滿足類似的行為 :
index(arr, n) = value;
C++ 11 中那個帶有尾置回傳型別的函式確實可以滿足, 但是 C++ 14 中不帶有尾置回傳型別的函式就無法滿足了. 這是因為 auto
在推導的時候, 可能會忽略變數本身所帶有的 const
限定、volatile
限定和參考限定, 之前我們已經提到過了. 那麼表達式 index(arr, n)
的回傳值是一個右值, 我們是沒有辦法對右值進行指派的, 除非 T
型別多載了允許右值進行指派的運算子 (這幾乎沒有什麼意義). 但是為什麼 decltype
就可以呢? 因為 decltype
在推導的時候只是簡單地將表達式 arr[n]
的值對應的型別回傳而已, 並沒有 auto
那麼複雜. 為了解決這個問題, C++ 14 引入了 decltype(auto)
. 也就說, 函式的回傳型別仍然是 auto
, 也就是需要編碼器幫助推導, 但是推導的規則使用 decltype
的推導規則 :
template <typename T>
decltype(auto) index(T *arr, int n) {
return arr[n];
}
這樣就沒什麼問題了. 但是有些容器也多載了陣列注標運算子, 因此我們可能希望上述函式同樣可用於容器, 那麼有 :
template <typename Container>
decltype(auto) index(Container &arr, int n) {
return arr[n];
}
我並沒有為第一個參數添加 const
限定, 這是因為我還想讓函式支援
index(arr, n) = value;
的行為. 但是有些容器的右值同樣支援陣列注標運算子, 所以我們希望函式 index
可以支援某些右值容器. 但是右值是不可以繫結到不帶 const
限定的左值參考上的, 所以我們使用 C++ 11 的通用參考, 以參考折疊的方式實作函式 (這就是為什麼我剛才說對 auto
進行推導的時候, 可能會忽略 const
限定、volatile
限定 和參考限定了) :
template <typename Container>
decltype(auto) index(Container &&arr, int n) {
return arr[n];
}
但是這個函式的行為對於右值來說, 不正確. 這是因為即使是右值, 在函式 index
內部, arr
被視為左值, 其內部多載的陣列注標運算子回傳的型別是一個左值參考, 此處按照 decltype
的推導規則, 最終回傳型別就是某一個型別的左值參考. 當我們真正進行指派的時候, 那個右值很可能已經不存在了, 此時就會發生未定行為. 因此我們要借助 std::forward
:
#include <utility>
template <typename Container>
decltype(auto) index(Container &&arr, int n) {
return std::forward<Container>(arr)[n];
}
我們使用 std::forward
向編碼器保證 arr
的型別被完美轉遞, 使用一個右值容器的陣列注標運算子所得到的回傳值應該是一個右值參考, 從而保證
index(vector<int> {1, 2, 3, 4, 5}, 2) = value;
類似於這樣的行為和左值一樣可以正常運作, 雖然它沒有任何意義. 另外, 這裡有個補充. 有些人可能會疑惑當函式 index
結束的時候, vector<int> {1, 2, 3, 4, 5}
不是會被銷毀嗎? 為什麼這裡可以指派. 特別指出, 函式呼叫的時候產生的臨時物件, 是等整條陳述式運作完畢之後才被銷毀. 也就是說 vector<int> {1, 2, 3, 4, 5}
這個臨時物件的生命週期一直到 (vector<int> {1, 2, 3, 4, 5}).operator[](2) = value
這條表達式運作完畢才開始銷毀
除此之外, 有一個例外需要閣下注意, delctype(x)
和 delctype((x))
所推導的結果是不同的, 這我早在《C++ 學習筆記》中就提到過了, 其中 x
是一個變數的名稱. 因此, 不要作死寫出這種程式碼 :
template <typename Container>
decltype(auto) index(Container &&arr, int n) {
auto value {arr[n]};
//...
return (value);
}
這個程式碼自帶未定行為的特效, 和下面的程式所得到的結果完全不同 :
template <typename Container>
decltype(auto) index(Container &&arr, int n) {
auto value {arr[n]};
//...
return value;
}
對於 auto
, 我的建議和 Scott Meyers 的建議一樣 : 在可以使用 auto
的情況下, 儘量採用 auto
; 只有 auto
推導可能有誤或著無法使用 auto
的時候, 才明確寫出其型別
我直接拿《Effective Modern C++》中的三個實例來作說明, 首先是對於 lambda 表達式. C++ 11 引入的 lambda 表達式, 你是不知道它的型別的, 它的型別只有編碼器才知道, 因此我們只能使用 auto
來宣告一個 lambda 表達式 :
auto lambda {[]() noexcept -> auto {}};
但是除了 auto
之外, std::function
也有能力接收一個 lambda 表達式 :
std::function<void () noexcept> f([]() noexcept -> auto {});
你可能為了同一行為, 所以把所有的 lambda 表達式都使用 std::function
包裝起來. 但是事實上, 這確實會影響程式的運作效率和程式大小. 首先, std::function
物件有著固定的尺寸 :
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
cout << sizeof(std::function<void ()>) << endl; //48
cout << sizeof(std::function<int &()>) << endl; //48
cout << sizeof(std::function<void *(bitset<200> &, size_t)>) << endl; //48
cout << sizeof(std::function<void (int, char, double, long, char32_t)>) << endl; //48
}
上面的程式碼輸出不約而同都是 48
, 無論 std::function
接受一個什麼樣的可呼叫物件. 但是對於有些可呼叫物件, 48 個位元組的大小可能無法存下它, 於是 std::function
便使用 new
來配置額外的記憶體用於儲存. 另外, 編碼器有時候會限制 std::function
的內嵌函式請求, 從而導致函式呼叫上面的效率損失. 因此, 直接使用 auto
宣告一個 lambda 表達式而得到的可呼叫物件幾乎比使用 std::function
得到的可呼叫物件成本要小, 而且運作效率更高. 在之後的 C++ Standard Template Library 文章中, 你將會見到 std::function
的實作方式
lambda 表達式的型別我們完全不知道, 但是有些函式的回傳值型別我們可以通過查閱文獻知道它, 但是一般由於懶或著不注重細節, 所以我們忽略它 :
#include <vector>
using namespace std;
int main(int argc, char *argv[]) {
vector vec {1, 2, 3};
int size {vec.size()};
}
雖然不一定, 但是上述程式碼幾乎無法通過編碼, 因為 C++ 標準規定了 vector 的成員函式 size
必須回傳一個無號數型別. 你認為修改這個程式碼非常簡單 :
#include <vector>
using namespace std;
int main(int argc, char *argv[]) {
vector vec {1, 2, 3};
unsigned size {vec.size()};
}
但是也不一定通過編碼. 這是因為 C++ 標準只規定了 vector 的成員函式 size
的回傳值必須是一個無號數型別, 但是無號數型別至少有 unsigned char
, unsigned short
, unsigned int
, unsigned long
和 unsigned long long
. 如果 size
回傳值的型別是 unsigned long
, 那麼這幾乎無法通過編碼, 因為 unsigned long
到 unsigned int
的型別轉換需要 static_cast
的幫助. 你再次認為這個程式碼的修改非常簡單 :
#include <vector>
using namespace std;
int main(int argc, char *argv[]) {
vector vec {1, 2, 3};
unsigned long long size {vec.size()};
}
我乾脆使用最大的型別來接收它, 這下不會錯了吧? 但是, 很多編碼器擁有一個更大的內建型別 : __uint128_t
. 在 macOS 10.14 的 Apple LLVM Clang++ 下, sizeof(unsigned long long)
的回傳值為 8
, 而 sizeof(__uint128_t)
的回傳值為 16
. 你能保證程式庫的作者不使用 __uint128_t
或著乾脆自己實作一個 vector::size_type
嗎? 除了編碼錯誤, 有時候還會出現運作效率的損失甚至未定行為. 因此, 此處應該使用 auto
:
#include <vector>
using namespace std;
int main(int argc, char *argv[]) {
vector vec {1, 2, 3};
auto size {vec.size()};
}
最後一個實例是關於 std::map
的 :
#include <map>
#include <string>
using namespace std;
int main(int argc, char *argv[]) {
map<string, int> m;
for(const pair<string, int> &value : m) {
//...
}
}
這個程式碼有問題嗎? 如果沒有經歷過, 你幾乎不可能看出問題, 即使你是 C++ 老手. 但是上述程式碼確實存在問題 : 我們給定 map
的 key_type
是 string
, 但是由於 STL 的 map
預設 key_type
不可以發生變更, 所以到了 map
裡面, string
就變成了 const string
. 也就是說, map
中的每一個物件, 其真正的型別是 std::pair<const string, int>
. 那麼上述程式碼就會出現每進行一次迴圈, 就會多運作一次 std::pair<const string, int>
到 std::pair<string, int>
的複製, 在每一次迴圈結束的時候, 又會去解構剛剛所複製的物件. 你可能覺得奇怪, 按道理來說 std::pair<const string, int>
到 std::pair<string, int>
的型別轉換並不成立, 至少針對這個 pair_t
:
template <typename T, typename U = T>
struct pair_t {
T first;
U second;
};
沒錯, 這樣簡單的 pair_t
是不成立的, 但是 C++ 標準樣板程式庫中的 pair
可不是這個樣子, 它比上面的 pair_t
複雜得多. 具體是如何實作的, 我到時候會在 C++ Standard Template Library 欄目中撰寫文章講述 std::pair
的實作. 現在你只需要知道, 對於 std::pair
來說, 從std::pair<const string, int>
到 std::pair<string, int>
的型別是成立的
現在你應該明白, 之前程式碼中的 value
是個復件, 對它的所有操作都不對原來的 map
生效. 因為這是一次右值繫結到被 const
限定的變數上, 所以去掉 const
, 編碼器就會擲出編碼錯誤 :
#include <map>
#include <string>
using namespace std;
int main(int argc, char *argv[]) {
map<string, int> m;
for(pair<string, int> &value : m) //non-const lvalue reference to type 'pair<basic_string<...>, [...]>' cannot bind to a value of unrelated type 'pair<const basic_string<...>, [...]>'
{
//...
}
}
因此, 我們使用 auto
讓編碼器進行推導 :
#include <map>
#include <string>
using namespace std;
int main(int argc, char *argv[]) {
map<string, int> m;
for(const auto &value : m)
{
//...
}
}
如果需要對 value
進行改變的操作, 那麼去掉 const
限定即可
上面三個實例都令你有足夠的理由優先選用 auto
, 而不是明確地宣告其型別. 而且這三個實例中的 auto
有一個共通的點, 就是打字更少. auto
只有四個字元, 而對於最後一個實例, 如果實作正確, 你需要鍵入 pair<const string, int>
以替換 auto
, 這還是在 using namespace std;
的情況下
最後給出兩個實例說明並不是每一個地方都適合使用 auto
. 一個比較簡單的實例, 函式 size
:
#include <stdexcept>
template <typename Container>
typename Container::size_type size(const Container &c) {
auto sz {0};
for(auto i {0};;) {
try {
c[i++];
}catch(std::out_of_range &) {
break;
}
}
return sz;
}
某一個容器的作者對陣列注標的引數進行了檢查, 但是這個容器卻沒有提供應該提供的成員函式 size
, 用於回傳容器中目前有多少元素, 於是我們使用上述程式碼來回傳一個容器中的元素數量. 一般來說, 這個函式運作地比較好, 它的回傳型別取決於容器 Container
中的 size_type
別名. 函式中的局域變數 sz
被編碼器推導為 int
, 假如 typename Container::size_type
並不支援從 int
到它的任何一種可能的轉換 (包括複製建構、複製指派和可能的多載型別轉換函式), 那麼上述函式就會出現編碼錯誤. 這個時候, 就不能直接使用 auto
, 而是應該明確標識它的型別 :
template <typename Container>
typename Container::size_type size(const Container &c) {
typename Container::size_type sz {};
for(auto i {0};;) {
try {
c[i++];
}catch(std::out_of_range &) {
break;
}
}
return sz;
}
一般來說, size_type
的預設初始化會使得 sz
的值為 0
. 如果你不放心, 也可以這樣寫 :
typename Container::size_type sz {0};
或著這個 typename Container::size_type
的作者再狠一些, 不支援隱含型別轉換, 那麼你可以這麼寫 :
auto sz {static_cast<typename Container::size_type>(0)};
或著
auto sz {typename Container::size_type(0)};
上面兩個我們使用了 auto
是因為我們在後面已經標識了它的型別, 編碼器也很容易從後面的型別中識別 sz
的型別, 再寫多一次就顯得重複
另外一個實例是關於代理類別的. 所謂代理類別就是 C++ 中, 我們會實作出一些類別, 使得它的行為像某一些類別或著內建型別一樣. 比如疊代器和智慧指標就是用於模擬內建指標的. 這個實例特別使用 C++ 標準樣板程式庫中大家都不推薦使用的 std::vector<bool>
(C++ 標準樣板程式庫中的 vector
針對 bool
型別進行特製化, 使得它更節省記憶體, 使用 1 個位元來表示 bool
) :
#include <vector>
#include <random>
using namespace std;
vector<bool> random_boolean_list(size_t n) {
default_random_engine e(static_cast<unsigned>(time(nullptr)));
bernoulli_distribution b;
vector<bool> list {};
while(n--) {
list.push_back(b(e));
}
return list;
}
void process(int n, bool &status) {
//...
}
int main(int argc, char *argv[]) {
auto boolean {random_boolean_list(100)};
int n {};
//do something for n
//...
if(n >= 100) {
throw std::out_of_range("n is out of range!");
}
auto bit {boolean[n]};
process(n, bit); //Compile Error : no matching function for call to 'process'
}
上述程式碼首先隨機產生一個布林動態陣列, 然後取其中一個進行處理. 看著沒有什麼問題的程式碼實際上出現了編碼錯誤. 我們從編碼錯誤中, 大致可以知道 bit
並不是 bool
型別或 bool &
型別, 更不是 bool &&
型別. 此處我們通過一個小技巧, 讓編碼器在編碼錯誤的同時告訴我們 bit
是什麼型別 :
++bit;
Clang 是這樣告訴我的 : cannot increment value of type 'std::__1::__bit_reference<std::__1::vector<bool, std::__1::allocator<bool> >, true>'
. 也就是說, bit
並不是 bool
型別, 而是一個自訂類別. 通過 vector<bool>
的一些行為, 我們也知道這個類別在模擬 bool
型別的行為, 也就是我們上面所說的代理類別. 因此, 我們知道了為什麼函式沒有辦法匹配了. 你可能會說, 這個回傳的型別不是在模擬 bool
型別的行為嗎? 那麼它必定可以向 bool
型別發生隱含型別轉化. 確實可以 :
#include <vector>
#include <random>
using namespace std;
vector<bool> random_boolean_list(size_t n) {
default_random_engine e(static_cast<unsigned>(time(nullptr)));
bernoulli_distribution b;
vector<bool> list {};
while(n--) {
list.push_back(b(e));
}
return list;
}
void process(int n, bool &status) {
//...
}
int main(int argc, char *argv[]) {
auto boolean {random_boolean_list(100)};
int n {};
//do something for n
//...
if(n >= 100) {
throw std::out_of_range("n is out of range!");
}
bool bit {boolean[n]};
process(n, bit); //OK
}
這下沒有什麼問題了. 如果函式 process
接受的引數型別為 bool
而非 bool &
:
#include <vector>
#include <random>
using namespace std;
vector<bool> random_boolean_list(size_t n) {
default_random_engine e(static_cast<unsigned>(time(nullptr)));
bernoulli_distribution b;
vector<bool> list {};
while(n--) {
list.push_back(b(e));
}
return list;
}
void process(int n, bool status) {
//...
}
int main(int argc, char *argv[]) {
constexpr auto size {100};
int n {};
//do something for n
//...
if(n >= 100) {
throw std::out_of_range("n is out of range!");
}
auto bit {random_boolean_list(size)[n]};
process(n, bit); //undefined behaviour
}
這下即使使用 auto
, 也同樣沒有什麼問題. 但是函式 random_boolean_list
回傳的 vector<bool>
臨時物件已經被回收了, 但是 bit
內部必定還要指向 vector<bool>
臨時物件中的某個位元, 對 bit
的任何操作都會導致未定行為. 如果你想要知道具體的原因, 那麼就必須了解 std::vector<bool>
是如何設計的 :
vector<bool>
和用於模擬 bool
型別的類別都是使用了特殊的實作方式. 首先我們知道, 任何一個 C++ 標準程式庫的容器或著陣列使用它們的 operator[]
, 回傳的必定是一個型別的參考, 針對 vector<bool>
來說本來應該是 bool &
. 但是由於 vector<bool>
為了節省記憶體空間, C++ 內建型別中也不包含類似於 bit
這樣的位元流型別, 所以將一個 8 個位元組大小的 unsigned char
(一般都是它) 分成 8 個位元使用, 因此需要使用自訂型別來模擬位元流. 一種比較簡單的實作方式如下 :
//bit stream implement
template <bool IsConst>
class bit_reference {
private:
unsigned char *arr;
size_t mask;
public:
bit_reference(char *, size_t);
public:
operator bool() const noexcept {
return static_cast<bool>(*this->arr & this->mask);
}
//...
};
template <>
class bit_reference<true> {
//...
};
//vector implement
template <typename T> class vector;
template <>
class vector<bool> {
public:
using reference = bit_reference<false>;
using const_reference = bit_reference<true>;
private:
unsigned char *start;
unsigned char *end;
size_t offset;
public:
reference operator[](int n) const noexcept {
return reference(start, n);
}
};
從上述程式碼中, 我們可以看到, vector<bool>
回傳的是 bit_reference
, 而不是 bool &
, 儘管 bit_reference
可以隱含地向 bool
型別轉化. 但是你要清楚, 當我們呼叫函式 random_boolean_list
的時候, 獲得的是一個型別為 vector<bool>
的臨時物件 (不妨記為 temp
), 然後取其第 n
個. 這條陳述式運作完畢之後, temp
就會被回收, 也就是其內部配置的記憶體的 unsigned char *start
到 unsigned char *end
都會被收回. 但是 temp[n]
所對應的 bit_reference
物件 (不妨記為 bit_temp
) 中儲存的是 [start, end) 範圍中的地址, 由於回傳的 temp
已經被回收了, 因此對 bit_temp
的任何操作都會導致對已收回而未重新配置的記憶體位址進行訪問, 從而導致未定行為
通過上面的論述, 我們幾乎可以明白, 任何對代理類別使用 auto
進行推導很可能會導致一些未定行為甚至嚴重錯誤. 那麼我們是否應該放棄 auto
呢? 完全不是, 你可以這麼做 :
#include <vector>
#include <random>
using namespace std;
vector<bool> random_boolean_list(size_t n) {
default_random_engine e(static_cast<unsigned>(time(nullptr)));
bernoulli_distribution b;
vector<bool> list {};
while(n--) {
list.push_back(b(e));
}
return list;
}
void process(int n, bool status) {
//...
}
int main(int argc, char *argv[]) {
constexpr auto size {100};
int n {};
//do something for n
//...
if(n >= 100) {
throw std::out_of_range("n is out of range!");
}
auto bit {static_cast<bool>(random_boolean_list(size)[n])};
process(n, bit); //OK
}
或著採用之前直接明確宣告 bool
的方式, 就完全沒有問題了. 那麼對於每一個變數都這麼處理和不使用 auto
有什麼區別? 唯一的區別就是你需要多多閱讀程式庫作者所撰寫的文檔, 至少對要用到的函式或著類別有一個基本的印象, 知道哪些地方可能會產生這樣的錯誤. 當遇到這樣的地方, 我們就使用明確宣告型別的方式替代使用 auto
進行推導的方式, 而你肯定也有經驗, 這種情況並不多. 另外, 多了解程式庫的評價. 比如 C++ 標準樣板程式庫中的 std::vector<bool>
被稱為 C++ 標準樣板程式庫中最大的坑. 幾乎知道的人都會建議新人不要使用它, 而且有不少激進的人直接建議 C++ 標準委員會廢除 std::vector<bool>
這樣的偏特製化 (實際上, 它還有另外一個樣板參數用於記憶體配置). 但是由於歷史原因, C++ 並沒有廢除 std::vector<bool>
導致現在它還留在 C++ 標準樣板程式庫中. 但是經過我們基本的了解之後, 你會發現這是一個比較好的節省記憶體的設計, 只不過在用的時候有些時候會有陷阱, 這對於任何容器的疊代器而言都會有不是嗎? 小心地使用它們, 並不會導致任何問題. 說到底只是水平問題罷了
對於 std::vector<bool>
, 我們一般使用 std::bitset
來代替它
自創文章, 原著 : Jonny, 如若需要轉發, 在已經授權的情況下請註明出處 :《【C++】auto 與 decltype》https://jonny.vip/2020/07/04/%e3%80%90cplusplus%e3%80%91auto-%e8%88%87-decltype/
Leave a Reply