一. 背景
項目中需要對數據庫查詢訪問的業務,在寫數據庫sql語句代碼時,由於沒有特別復雜的格式化需求,決定采用C++標准庫中的stream來進行sql語句的格式化。有兩點好處:
1). 類型安全
2). 使用方便
比如
std::ostringstream os;
std::uint32_t id = 10;
os << "select * from user_t where id = " << id;
sql_excute(os.str());
由於數據庫對輸入的字符串參數需要加上單引號,導致在格式化字符串參數的時候需要額外調用一個增加單引號的函數,如下:
std::string add_quote(const std::string ¶m)
{
return "'" + param + "'";
}
void print()
{
std::ostringstream os;
std::uint32_t id = 10;
std::string name = "test_name";
os << "select * from user_t where id = " << id << " and "
<< "name = " << add_quote(name);
sql_excute(os.str());
}
這樣是可以解決問題,但是使用起來非常繁瑣,特別是遇到有很多字符串參數的時候,要額外增加很多add_quote的調用,單單從實用角度來講就特別不方便,很有可能會漏掉一些字符串參數。於是,我就希望能通過某種技術手段,來規避掉這個問題(C++程序員最大的毛病就是喜歡去創造輪子而不務正業)。
二. 需求
1). 對operator<<(std::ostringstream &, const std::string &msg)中,對傳入的msg自動加上單引號
2). 對其他類型的重載保持原有語義
三. 解決方案
基於以上兩點,僅僅需要重載掉operator<<即可,各位看官請看
struct sql_ostream
: std::wostringstream
{};
sql_ostream &operator<<(sql_ostream &os, const std::wstring &msg)
{
std::wostringstream &tmp = os;
tmp << L"'" << msg << L"'";
return os;
}
template < typename T >
sql_ostream &operator<<(sql_ostream &os, T msg)
{
std::wostringstream &tmp = os;
tmp << msg;
return os;
}
對於std::wstring做特殊處理,其他類型的參數則調用基類進行默認處理。看上去一切正常,是真的嗎?未必!
當我們遇到額外的需求時,就會失效了,來看看這個例子:
struct AA
{
template < typename CharT >
operator std::basic_string<CharT>()
{
return std::basic_string<CharT>();
}
};
os << AA();
在這裡,我們的AA提供隱式轉換(別罵我腦殘干嘛要用這招,其實也有好處的)到basic_string。哈哈,現在不行了吧,編譯期報錯了吧。由於在此次隱式轉換中,需要根據接受字符串對象是char還是wchar_t而是一個模版參數,所以,os << AA()並不會選擇operator<<(sql_ostream &os, const std::wstring &),而是選擇了接受模版參數的第二個重載中,為什麼呢?
四. 函數重載的定義
這裡引出了一個ADL(Koenig)查找。
是指在編譯器對無限定域的函數調用進行名字查找時,所應用的一種查找規則。
首先來看一個函數所在的域的分類:
1 :類域(函數作為某個類的成員函數(靜態或非靜態))
2 :名字空間域
3 :全局域
而 Koenig 查找,它的規則就是當編譯器對無限定域的函數調用進行名字查找時,除了當前名字空間域以外,也會把函數參數類型所處的名字空間加入查找的范圍。
ADL 就是為了確保使用類型 X 的對象 x 時能夠像使用 X 的成員函數一樣簡單 (ensure that code that uses an object x of type X can use its nonmember function interface as easily as it can use member functions)
這裡是wiki上的解釋,這裡是Heber Surte的一個例子,而這裡是一篇CSDN的翻譯。
在我們這裡,因為涉及到模版函數匹配的問題,又引入了一個SFINAE原則(匹配錯誤不算失敗)。當然,有兩個准則需要記住:
1. 函數模板特化並不參與重載決議。只有在某個主模板被重載決議選中的前提下,其特化版本才有可能被使用。而且,編譯器在選擇主模板的時候並不關心它是否有某個特化版本
2. 如果一個普通的非模板函數跟一個函數模板在重載解析的參數匹配中表現一樣好的話,編譯器會選擇普通函數。
這裡是wiki對SFINAE的解釋。
五. 再次嘗試
好了,知道原因了,來看看如何解決掉這個問題。為了能在編譯期決策,我們需要引入一個間接層和一個編譯期選擇重載的模版組件std::enable_if。關於std::enable_if的文章,可以看這裡,這裡,這裡。趕緊來看看我們的代碼如何處理
namespace stdex
{
typedef std::wostringstream tOstringstream;
typedef std::wstring tString;
}
struct sql_ostream
{
stdex::tOstringstream os_;
stdex::tString str() const
{
return std::move(os_.str());
}
struct serialize_impl
{
template < typename T >
static sql_ostream &to(sql_ostream &os, T msg,
typename std::enable_if<
std::is_arithmetic<T>::value ||
std::is_pointer<T>::value
>::type *N = 0)
{
static_assert(std::is_arithmetic<T>::value || std::is_pointer<T>::value,
"must a arithmetic or pointer type");
os.os_ << msg;
return os;
}
static sql_ostream &to(sql_ostream &os, const stdex::tString &msg)
{
os.os_ << _T("'") << msg << _T("'");
return os;
}
};
template < typename T >
sql_ostream &serialize(T &&msg)
{
return serialize_impl::to(*this, msg);
}
};
template < typename T >
sql_ostream &operator<<(sql_ostream &os, T &&msg)
{
os.serialize(msg);
return os;
}
這裡有幾點需要注意:
1). 並沒有采用繼承自std::stringstream
2.) template < typename T > sql_ostream &operator<<(sql_ostream &os, T &&msg) 一個暴露對外的接口,參數傳遞采用的是&&
3). serialize_impl中重載了to,第一個to函數的第三個參數是根據std::enable_if來推斷的
對於第1點,繼承是條賊船,上去了就下不來
對於第2點,使接口簡單,這裡只是起到一個完美轉發的作用,可以右值引用
對於第3點,看過前面給出的鏈接,大家應該都知道enable_if的作用,就是為了消除掉模版類或者模版函數在重載時的二義性
這樣,就可以使AA可以完美的通過隱式轉換進入到to(sql_ostream &os, const stdex::tString &msg) 中。
六. 總結
C++的重載決議是個很復雜的過程,特別是用在模版函數或者模版類的情況下更復雜,為了讓編譯器知道更多地類型信息,我們引入了std::enable_if,對於重載就會稍微簡單點。