返回值生命期
在本章,我們通過考察5個不完美的將整形轉換為字符串類型的技術,探討下C++中返回值生命期的各種問題和陷阱,最后總結垃圾收集機制的一個實際價值。
- integer_to_string<>
template<typename C>
C const* integer_to_string(C* buf, size_t cchBuf, sint8_t i)
{
return signed_integer_to_string(buf, cchBuf, i);
}
...
template<typename C>
C const* integer_to_string(C* buf, size_t cchBuf, uint8_t i)
{
return unsigned_integer_to_string(buf, cchBuf, i);
}
template<typename C, typename I>
inline const C *signed_integer_to_string(C* buf, size_t cchBuf, I i)
{
C* psz = const_cast<C*>(unsigned_integer_to_string(buf, cchBuf, i));
if (i < 0)
{
--psz;
*psz = C('-');
}
return psz;
}
template<typename C, typename I>
inline const C* unsigned_integer_to_string(C* buf, size_t cchBuf, I i)
{
C* psz = buf + cchBuf - 1;
*psz = 0;
do
{
unsigned lsd = i % 10;
i /= 10;
--psz;
*psz = get_digit_character<C>()[lsd];
} while (i != 0);
return psz;
}
template<typename C>
const C* get_digit_character()
{
static const C s_characters[19] =
{
'9', '8', '7', '6', '5', '4', '3', '2', '1',
'0',
'1', '2', '3', '4', '5', '6', '7', '8', '9'
};
static const C* s_mid = s_characters + 9;
return s_mid;
}
該方案效率很高,僅相當于spintf()開銷的10%。然而,也并非完美無缺。首先,它不算十分簡潔:需要用戶提供緩沖區的指針和緩沖區的長度。其次,更重要的一點,用戶提供的緩沖區長度有可能是錯誤的。如果太小,也許還能在調試版中引發一個斷言,如果大于緩沖區真實的長度,就有可能發生緩沖區溢出。
- TSS
利用TSS(Thread-Specific Storage,線程相關存儲),我們可以提供一個線程安全的內部緩沖區,就只需提供待轉換的整型值一個參數即可了。
template<typename C, typename I>
C const* int_to_string(I i)
{
const size_t CCH = 21; // 足以容納64位有符號整型的最大值
C * buf = get_tss_buffer<C, CCH>();
return integer_to_string(buf, CCH, i);
}
template<typename C, size_t CCH>
C* get_tss_buffer()
{
__declspec(thread) static C s_buffer[CCH];
return s_buffer;
}
注:__declspec(thread)是微軟Win32編譯器提供的擴展,如果需要跨平臺或跨編譯器,可以考慮選擇其他合適的TLS庫。
該方案有個小小的不便就是,無法推導字符類型了,需要顯式地參數化函數模板。如:int_to_string<char>(10)。
然而,還有個更顯著的缺陷,就是無法再單個表達式中被多次調用。如:printf("%s, %s", int_to_string<char>(5), int_to_string<char>(10)); - 擴展RVL
為了解決方案2的在同一表達式中不能連續調用的問題,可以運用一個技巧讓其沖突的可能性降低。
template<typename C, size_t CCH>
C* get_tss_buffer()
{
const size_t DEGREE = 32;
__declspec(thread) static C s_buffer[DEGREE][CCH];
__declspec(thread) static size_t s_index;
s_index = (s_index + 1) % DEGREE;
return s_buffer[s_index];
}
這里的32只是一個猜測值,即假定單個表達式中最多不超過32個同一類整型到字符串的轉換。32表示“安全”和棧大小之間的一個折衷,相對于方案2給用戶提供了一種虛假的安全感,所以并不推薦使用。
- 靜態數組大小決議
針對方案1,我們利用編譯器能夠推導出數組的靜態大小。我們可以定義integer_to_string()函數的一個新的重載版本,它接受一個數組為參數,而不是自帶緩沖區長度的指針。
template<typename C, size_t N>
C const* integer_to_string(C(&buf)[N], sint8_t i)
{
STATIC_ASSERT(!(N < printf_traits<sint8_t>::size));
return integer_to_string(buf, cchBuf, i);
}
該方案消除了錯誤的緩沖區長度被傳遞給函數的可能性,更妙的是,我們可以使用靜態斷言來進行編譯器檢查,從而確保緩沖區長度是足夠的。該方案是線程安全的,且不需要進行任何顯式實例化,如:
uint64_t i = ...
char buff[12];
char const* s = integer_to_string(buff, i);
這個方案唯一的缺點就是我們仍然還得提供一個緩沖區。
- 轉換墊片
最后一個解決方案,不再像前面的解決方案一樣返回一個指針,它走了另一條不同的路:使用一個代理類,返回該代理類的實例。當然,幕后仍然是integer_to_string()函數在承擔著實際的轉換工作。
template<typename C, typename I>
class int_to_string_proxy
{
public:
typedef C char_type;
typedef I int_type;
public:
int_to_string_proxy(int_type i)
: m_result(integer_to_string(m_sz, dimensionof(m_sz), i))
{}
int_to_string_proxy(int_to_string_proxy const& rhs)
: m_result(m_sz)
{
char_type* dest = m_sz;
char_type const* src = rhs.m_result;
while (0 != (*dest++ = *src++))
{}
}
operator char_type const* () const
{
return m_result;
}
private:
char_type const * const m_result;
char_type m_sz[21];
private:
int_to_string_proxy& operator =(int_to_string_proxy& rhs);
};
template<typename C, typename I>
int_to_string_proxy<C, I> int_to_string(I i)
{
return int_to_string_proxy<C, I>(i);
}
與所有轉換墊片一樣,該方案受到RVL-PDP(析構后指針)問題的困擾。它需要在表達式中立即使用返回值,而不應該保留到以后使用。
在這里,我們發現了一個垃圾收集機制真正能夠發揮無可比擬的地方。借助于垃圾收集,前面討論的RVL(返回值生命期)問題迎刃而解。由于垃圾收集的工作機制是釋放那些不再被活躍代碼所引用的內存,所以毫無疑問,當一個指向堆分配的內存塊的指針在仍然被需要的時候是被會失效的。我們所要做的就是返回一個新分配的堆內存塊而已。
多維數組
C/C++不支持各維大小都是動態的多維數組,但我們可以借助語言內建對多維數組進行“切片”的能力實現一個自定義的多維數組類(容器)。
作者給出的fixed_array系列多維數組類模板具有以下幾個顯著的特征:
- 每一個模板都維護一個指向一維內存快的指針,其他存放的是邏輯上N維的數組元素。
- 每一個類都有一個dimension_element_type成員類型,該成員的類型對一維數組類其實就是value_type,對高維數組類就是比它低一維的數組模板。
- begin()和end()方法返回的迭代器表示整個多維數組所有的元素集合區間,這便意味著,可以是使用同一stl算法(如 std::for_each())來處理不同維數的數組。
- 一個數組模板不但自身能夠作為一個完整的多維數組類,而且還能夠作為比它更高維的數組模板的子數組切片。作為完整多維數組類時,其模板參數R為true,其標準構造函數會分配相應的內存。作為其他更高維度數組的子數組切片時,其模板參數R為false,其切片構造函數只是接受一個相應的子數組切片指針。
template<typename T,
typename A = typename allocator_selector<T>::allocator_type,
typename P = do_construction<T>,
bool R = true>
class fixed_array_2d : protected A, public stl_collection_tag
{
public:
typedef fixed_array_2d< T, A, P, R > class_type;
typedef fixed_array_1d< T, A, P, false > dimension_element_type;
typedef A allocator_type;
typedef T value_type;
typedef value_type & reference;
typedef value_type const & const_reference;
typedef value_type * pointer;
typedef value_type const * const_pointer;
typedef size_t size_type;
typedef size_t index_type;
typedef ss_ptrdiff_t difference_type;
typedef bool bool_type;
typedef pointer_iterator< value_type, pointer, reference >::type iterator;
typedef pointer_iterator< value_type const , const_pointer, const_reference >::type const_iterator;
// 構造函數
private:
// 切片構造
fixed_array_2d(T *data, index_type d0, index_type d1);
public:
// 標準構造
fixed_array_2d (index_type d0, index_type d1);
fixed_array_2d (index_type d0, index_type d1, allocator_type const &ator);
fixed_array_2d (index_type d0, index_type d1, value_type const &t);
fixed_array_2d (index_type d0, index_type d1, value_type const &t, allocator_type const &ator);
fixed_array_2d (class_type const &rhs);
~fixed_array_2d();
allocator_type get_allocator () const;
void swap (class_type &rhs); throw ();
// 訪問
public:
reference at (index_type i0, index_type i1);
const_reference at (index_type i0, index_type i1) const;
reference at_unchecked (index_type i0, index_type i1);
const_reference at_unchecked (index_type i0, index_type i1) const;
reference operator(); (index_type i0, index_type i1);
const_reference operator(); (index_type i0, index_type i1) const;
dimension_element_type at (index_type i0);
const dimension_element_type at (index_type i0) const;
dimension_element_type at_unchecked (index_type i0);
const dimension_element_type at_unchecked (index_type i0) const;
dimension_element_type operator[] (index_type i0);
const dimension_element_type operator[] (index_type i0) const;
reference front ();
reference back ();
const_reference front () const;
const_reference back () const;
pointer data ();
const_pointer data () const;
// 迭代
public:
iterator begin ();
iterator end ();
const_iterator begin () const;
const_iterator end () const;
// 狀態
public:
index_type dimension0 () const;
index_type dimension1 () const;
index_type size () const;
bool_type empty () const;
static size_type max_size ();
// 實現
private:
pointer allocate_(size_type n);
void deallocate_(pointer p, size_type n);
pointer data_();
index_type calc_index_(index_type i0, index_type i1) const;
void range_check_(index_type i0, index_type i1) const;
void range_check_(index_type i0) const;
allocator_type& get_allocator_();
// 成員
private:
T* m_data;
index_type m_d0;
index_type m_d1;
size_type m_size;
friend class fixed_array_3d<T, A, P, true>;
friend class fixed_array_3d<T, A, P, false>;
// 聲明但不實現
private:
class_type const& operator =(class_type const& rhs);
};
另外,C/C++對固定大小的數組支持是無容置疑的,但為了獲得通過bein()/end()來獲得對整個數組進行迭代的能力,我們也設計了一組模板類來模擬靜態多維數組。static_array自身并不進行任何形式的內存分配:如果它的身份是作為一個切片代理,它只會保存一個指向數組切片的指針;倘若它本身作為一個完整的數組,則會包含一個完整的N維內建數組。
template<typename T,
size_t N0,
size_t N1,
typename P = do_construction<T>,
typename M = T[N0 * N1]>
class static_array_2d : public null_allocator<T>, public stl_collection_tag
{
...
private:
M m_data;
};
仿函數
泛化的仿函數 —— 類型隧道(Type Tunneling):是一種通過訪問墊片使兩個邏輯相關但物理不相關的類型能夠互操作的機制,墊片允許一個外部類型通過一個接口以一種可識別且兼容的形式呈現于內部類型的面前。
template<typename C,
typename A = C const *>
class is_large : public std::unary_function<A, bool>
{
public:
template<typename S>
bool operator ()(S const& file) const
{
return is_large_(c_str_ptr(file)); // c_str_ptr墊片
}
private:
static bool is_large_(C const* file)
{
...
}
};
glob_sequence gs("/usr/include/", "impcpp*");
std::count_if(gs.begin(), gs.end(), is_large<char>());
glob_sequenc_w gsw(L"/usr/include/", L"impcpp*");
std::count_if(gsw.begin(), gsw.end(), is_large<wchar_t>());
readir_sequence rs("/usr/include/");
std::count_if(rs.begin(), rs.end(), is_large<char>());
局部類
雖然某些老版本的編譯器不支持局部仿函數類用于STL算法(VS2008支持的還不錯),但當我們遇到回調枚舉API時,局部類是個非常不錯的選擇。
HWND FindChildById(HWND hwndParent, int id)
{
if (::GetDlgCtrlID(hwndParent) == id)
{
return hwndParent;
}
else
{
struct ChildFind
{
ChildFind(HWND hwndParent, int id)
: m_hwndChild(NULL)
, m_id(id)
{
::EnumChildWindows(hwndParent, FindProc, reinterpret_cast<LPARAM>(this));
}
static BOOL CALLBACK FindProc(HWND hwnd, LPARAM lParam)
{
ChildFind& find = *reinterpret_cast<ChildFind*>(lParam);
return (::GetDlgCtrlID(hwnd) == find.m_id) ?
(find.m_hwndChild = hwnd, FALSE) : TRUE;
}
HWND m_hwndChild;
int const m_id;
}find(hwndParent, id);
return find.m_hwndChild;
}
}