基於C++可變參數模板格式化字符串

遨遊於C++世界時,最討厭的當屬於對c-style的兼容🙄。html

在格式化字符串時,一般使用的是snprintf這個c函數。snprintfsprintf的安全版,可以避免緩衝區溢出。c++

char buf[1024] = {0};
std::string s = "Hello world";
snprintf(buf, sizeof(buf), "format str: %s", s.c_str());

snprintf接受的參數跟printf差很少,都是c-style的數據類型,如%s接受的是const char*類型的數據,這就須要咱們將std::string作一個轉換。這一個小小的轉換讓我以爲很是不爽,有沒有可能讓std::string在作某個函數的參數時能自動作轉換?甚至讓一個將一個普通的對象自動轉換成const char*類型?git

接下來咱們將利用c++11的可變參數模板(variadic templates)、參數包(parameters pack)、完美轉發(perfect forwarding)等特性來實現這一想法。github

參數列表完美轉發

寫C++代碼時,我滿腦子都是怎麼最大限度地提升性能。咱們此次的目標也同樣,在提供方便的同時,不要對性能有太大影響,甚至不影響。安全

首先是要將傳入fmt函數的參數完美轉發至snprintfbash

template<typename... Args>
string fmt(const char *format, Args&&... args) {
  char buf[128] = {0};
  snprintf(buf, sizeof(buf), format, convert(std::forward<Args>(args))...);
  return buf;
}

這是一個可變參數模板,args表示傳入的參數們。咱們的思路是將傳入的參數作一個轉換以後傳給snprintf函數

爲了原封不動的保持左右值引用,首先是用Args&&代替Args的參數類型,此處模板函數的Args須要編譯器推導,因此是一個通用引用(Universal reference),能夠指代左值或右值。用std::forward<Args>能保持參數的左右值性質,作到參數的完美轉發。post

自動參數轉換

convert這個函數中,咱們要將特定的類型轉換成const char*類型,而那些能被snprintf接受的類型如int, double, char*,則原封不動的返回。性能

convert函數針對不一樣的參數類型須要返回不一樣的類型。這裏也將返回值做爲一個模板類型便可。c++11

template<typename T>
struct item_return {
  using type = T&&;
};

convert函數的定義爲:

template<typename T>
inline typename item_return<T>::type convert(T&& arg) {
  return static_cast<T&&>(arg);
}

convert函數默認將傳入的參數原封不動的返回。接下來咱們要作模板的偏特化,對於指定的對象,將其轉換爲const char *類型

// lvalue
template<>
struct item_return<obj&> {
  using type = const char*;
};

template<>
inline typename item_return<obj&>::type convert<obj&>(obj &arg) {
  std::cout << "receive lvalue\n";
  return arg.s.c_str();
}

// rvalue
template<>
struct item_return<obj> {
  using type = const char*;
};

template<>
inline typename item_return<obj>::type convert<obj>(obj &&arg) {
  std::cout << "receive rvalue\n";
  return arg.s.c_str();
}

注意,返回值也是須要偏特化的。

最後

我構造了一個class,hook他的兩個構造函數以便於觀察是否發生了拷貝。

class obj {
public:
  string s;
  obj(const char * ss) {
    s = ss;
  }
  obj(const obj& other):s(other.s) {
    printf("copy constructor\n");
  }
  obj(obj&& other):s(other.s) {
    printf("move constructor\n");
    other.s.clear();
  }
};

以後咱們使用fmt函數,就能像格式化c-style字符串同樣,格式化任意一個對象啦。

int main() {
  obj a("haha");
  int b = 3;
  std::cout << fmt("%s %s\n%d %d", a, obj("xixi"), b, 2) << std::endl;
  return 0;
}

運行結果爲

receive lvalue
receive rvalue
haha xixi
3 2

很好,並無發生拷貝。

參考資料

相關文章
相關標籤/搜索