避免function argument implicit conversion的方式

在std::string中,如果我們想要assign char
例如

std::string s = ‘A’; 這樣是不行的

必須要 std::string s(1, ‘A’);

std::string的constructor 參考 http://www.cplusplus.com/reference/string/string/string/

string(); //default (1)	
string (const string& str); //copy (2)	
string (const string& str, size_t pos, size_t len = npos); //substring (3)	
string (const char* s); //from c-string (4)
string (const char* s, size_t n); // from buffer (5)	
string (size_t n, char c); //fill (6)	
template <class InputIterator> string  (InputIterator first, InputIterator last); //range (7)	
string (initializer_list<char> il); //initializer list (8)	
string (string&& str) noexcept; //move (9)

假想我們是string的設計者,加一個 string(char c); 會如何呢?

這樣看起來似乎完美方便,但其實在使用上多出了一些意想不到的情況

例如 std::string s = 123.4;

這樣就會走 string(char c);這條路而可以compile過,理由是Floating-integral conversions

An rvalue of a floating point type can be converted to an rvalue of an integer type. The conversion truncates; that is, the fractional part is discarded. The behavior is undefined if the truncated value cannot be represented in the destination type.

參考 C++03 4.9 Floating-integral conversions

另外還包括 std::string s = 123;

Integral conversion(非integral promotion)說明在integral type之間可以implicit互轉
An rvalue of an integer type can be converted to an rvalue of another integer type. An rvalue of an enumeration
type can be converted to an rvalue of an integer type.

參考: C++03 4.7 Integral conversions

因此開了一個char的路,導致floating point, integral type都可以初始化了
事實上 std::string s(1, ‘A’); 如果改成 std::string s(1, (long)65);也是可以compile過

在API的設計 我們如果想要避免insidious bug,若能在compile time對型態檢查發現就可以減少使用上潛在的問題。

以下參考 https://stackoverflow.com/questions/12877546/how-do-i-avoid-implicit-conversions-on-non-constructing-functions 整理C++11兩種做法

第一種作法是blacklist

利用deleted function讓compiler挑到該function而失敗。例如:
string(int v) = delete;
這種作法需要針對可能implicit conversion的case都要處理。以char的例子來說,我們需要排除掉各種可能的轉換,並且一旦定義了兩個可接受integral type的constructor — 以上面例子 char & int ,就要小心如果傳入的型態沒有exact match就會走promotion or conversion的路,這時候很可能會ambiguous。(當然,ambiguous也是compile不過,雖然不是我們希望走到的規則)

假如我們定義

string(char c);
string(int v) = delete;

string s = 2.3; //ambigous, floating point to integral conversion, choose char or int
string s = (short)2; //integral promotion, call string(int) delete function
string s = (long) 2; //ambigous, integral conversion, choose char or int

因此這種作法最好是要定義全面
將 int, unsigned int, long, unsigned long, long long, unsigned long long, double都 delete

不須對bool, unsigned char, short, unsigned short定義的原因是因為沒有exact match時會走integral promotion到int (但注意標準是說如果int可以容納得下上述的type的話,否則就會轉成unsigned int,這種情況在LP32-short, int都是2 bytes時會發生)

第二種做法是whitelist

直接用template把所有其他的type都delete

template <typename T>
string(T) = delete;

string s = ‘A’;會產生兩個選擇viable function。一個是string(char c); 另一個是template <> string(char) ,但是根據 overload resolution的規則

a viable function F1 is defined to be a better function than another viable function
F2 if…and then…or, if not that,
F1 is a non-template function and F2 is a function template specialization

C++03 13.3.3 Best Viable Function

因此,透過定義template deleted function的效果等於將沒有顯式定義的type的constructor都delete掉。

C++03沒有deleted function的機制,可以用以下方式讓compile fail

class DeleteOverload
{
private:
    DeleteOverload(void*);
};
template<typename T>
string(T, DeleteOverload = 0);

example:

#include <iostream>
#include <string>
/*
//C++03
class DeleteOverload
{
private:
    DeleteOverload(void*);
};
*/
class X
{
public:
  X(char c)
  {
  }
  template<typename T>
  X(T) = delete; //C++11
  /*
  //C++03
  template<typename T>
  X(T, DeleteOverload = 0);
  */
};
int main()
{
  X x = 1.23;
  return 0;
}
This entry was posted in C++ Language. Bookmark the permalink.

Leave a Reply