今天稍早, 跟一位同事聊到 Boost 的 lexical_cast. 打開了 Firefox 請出 Google, 想找到能幫助我介紹 lexical_cast 的資源. 意外地, Google 把我帶到讓我賺了介紹獎金, 現在跟我是同事的 Jeff, 談到 lexical_cast 的 文章.

在這篇文字的討論串, Jeff 寫道:

我覺得如果可以用這樣的寫法,那就太好了:

uint32_t ID = lexical_cast<auto>("1234567");

甚至最好是:

template <class From, class To = auto>
To lexical_cast(From s) {...}

uint32_t ID = lexical_cast("1234567");

Jeff Hung


小弟既然收了 Jeff 的好處, 當然得回報一下試著幫忙想想辦法: [1]

#include <sstream>
#include <iostream>
#include <stdexcept>

class lexi_cast
{
public:
  template <class Input>
  lexi_cast(const Input& in)
  {
    ss_ << in;
    if(!ss_.good())
      throw std::runtime_error("lexi_cast failed");
  }

  template <class Output>
  operator Output()
  {
    Output out;
    ss_ >> out;
    if(ss_.bad() || ss_.fail())
      throw std::runtime_error("lexi_cast failed");

    return out;
  }

private:
  std::stringstream ss_;
};

int main()
{
  using namespace std;

  // Converts C-style string to int.
  int i = lexi_cast("123");
  cout << i << endl;

  // Converts int to std::string.
  string s = lexi_cast(i);
  cout << s << endl;

  return 0;
}
    岔題一下.

    我一直不是很認同, 在 C++ 中, 以不同的規則來命名 class/function/object. 主要的原因是, 我認為這三者的分野其實並沒有那麼絕對.

    上面的 lexi_cast 就是一個很好的例子. 雖然它與 “真正” 的 function 相較仍有細微的不同, 但它的確是設計來讓使用者以 “function 風格” 使用的. 硬是把它的名字取得跟其他 function 格格不入, 豈不是很礙眼? 一個 function-like 的 instance (function instance, class instance or object instance) 可以在一個版本是真的 function, 在下一個版本卻變成 functional object, 甚至是如例中的 class instance.

    從另一個方面來看, 如果一個 “正常” class 設計的讓人容易把該 class 本身或所 instantiate 的 object 錯誤地當作 function 使用, 而必須以命名原則來區分, 其設計者更是應該要好好反省.

看起來這似乎已滿足了 Jeff 的基本需求, 只是這全自動的轉換機制也有它的問題. 有些時候, 不提供更多資訊給 compiler, 就是會有 ambiguity. 例如 stream insertion:

  // Ambigous! This won't compile!
  cout << lexi_cast(some_object);

Output stream 的 operator<<很多 overload. 由於 lexi_cast 提供了 generic 的 conversion operator, 因此 compiler 會找到多個可行 (viable) 的 overload. 若我們不顯性提示, compiler 會無法決定到底該採用哪個 overload. 這時候, lexi_cast 的全自動 (或說是缺乏手動指定的) 轉換功能反而變成了一種缺陷.

Attempt number 2. 簡單的把 lexi_cast 改成一個可以指定目的形別的 class template, 並配上做自動轉換的 default template parameter (struct auto_deduce) 以及 specialization:

struct auto_deduce {};

template <class Output = auto_deduce>
class lexi_cast2
{
public:
  template <class Input>
  lexi_cast2(const Input& in)
  {
    ss_ << in;
    if(!ss_.good())
      throw std::runtime_error("lexi_cast2 failed");
  }

  operator Output()
  {
    Output out;
    ss_ >> out;
    if(ss_.bad() || ss_.fail())
      throw std::runtime_error("lexi_cast2 failed");

    return out;
  }

private:
  std::stringstream ss_;
};

template <>
class lexi_cast2</*Output =*/auto_deduce>
{
public:
  template <class Input>
  lexi_cast2(const Input& in)
  {
    ss_ << in;
    if(!ss_.good())
      throw std::runtime_error("lexi_cast2 failed");
  }

  template <class Output>
  operator Output()
  {
    Output out;
    ss_ >> out;
    if(ss_.bad() || ss_.fail())
      throw std::runtime_error("lexi_cast2 failed");

    return out;
  }

private:
  std::stringstream ss_;
};

效果並不理想. 自動轉換 (default behavior of class template lexi_cast2) 雖不需要指定 type, 但還是需要角括號 (angled bracket), 用起來醜到爆:

int main()
{
  using namespace std;

  // Note the next (commented) line won't compile,
  // int i = lexi_cast2("123");

  // Angled brackets needed for auto deduction.
  // Not pretty at all.
  int i = lexi_cast2<>("123");
  cout << i << endl;

  string s = lexi_cast2<>(i);
  cout << s << endl;

  // Now we could specify which "type" to convert to.
  cout << lexi_cast2<int>(s) << endl;

  return 0;
}

Attempt number 3. 下面的程式中的 class template lexi_cast3_helper 與 specialization 是從前面的 lexi_cast2 修改過來的. 只增加了 copy-ctor: [2]

template <class Output>
class lexi_cast3_helper
{
public:
  template <class Input>
  lexi_cast3_helper(const Input& in)
  {
    ss_ << in;
    if(!ss_.good())
      throw std::runtime_error("lexi_cast3 failed");
  }
  // Copy-ctor. Needed for compilation, but not
  // invoked at run-time.
  lexi_cast3_helper(const lexi_cast3_helper& other)
  {
    ss_ << other.ss_.str();
  }

  operator Output()
  {
    Output out;
    ss_ >> out;
    if(ss_.bad() || ss_.fail())
      throw std::runtime_error("lexi_cast3 failed");

    return out;
  }

private:
  std::stringstream ss_;
};

template <>
class lexi_cast3_helper</*Output =*/auto_deduce>
{
public:
  template <class Input>
  lexi_cast3_helper(const Input& in)
  {
    ss_ << in;
    if(!ss_.good())
      throw std::runtime_error("lexi_cast3 failed");
  }
  // Copy-ctor. Needed for compilation, but not
  // invoked at run-time.
  lexi_cast3_helper(const lexi_cast3_helper& other)
  {
    ss_ << other.ss_.str();
  }

  template <class Output>
  operator Output()
  {
    Output out;
    ss_ >> out;
    if(ss_.bad() || ss_.fail())
      throw std::runtime_error("lexi_cast3 failed");

    return out;
  }

private:
  std::stringstream ss_;
};

lexi_cast3_helper 搭配上兩個 function template – 兩個 lexi_cast3 的 overload. 一個用來在自動轉換時躲避角括號 (angled bracket), 另一個讓使用者自行指定轉換的目的形別:

// Overload for output type auto deduction.
template <class Input>
lexi_cast3_helper<auto_deduce>
  lexi_cast3(const Input& in)
{
  return lexi_cast3_helper<auto_deduce>(in);
}
// Overload for user-specified output type.
template <class Output, class Input>
lexi_cast3_helper<Output>
  lexi_cast3(const Input& in)
{
  return lexi_cast3_helper<Output>(in);
}

結果是:

int main()
{
  using namespace std;

  // No ugly angled brackets for
  // auto deduction of output type.
  int i = lexi_cast3("123");
  cout << i << endl;

  string s = lexi_cast3(i);
  cout << s << endl;

  // User-specified output type.
  cout << lexi_cast3<int>(s) << endl;

  return 0;
}

這次看來就像樣多了. 能美美地做自動轉換, 也能自行指定轉換的目的形別.

The moral of this post – 果然還是 C++ 好玩!

[Update. Sep 22, 2009]

Improved and simplified. Per Jeff’s request:

class lexi_cast4_helper
{
public:
  template <class Input>
  lexi_cast4_helper(const Input& in)
  {
    ss_ << in;
    if(!ss_.good())
      throw std::runtime_error("lexi_cast4 failed");
  }
  // Copy-ctor. Needed for compilation, but not
  // invoked at run-time.
  lexi_cast4_helper(const lexi_cast4_helper& other)
  {
    ss_ << other.ss_.str();
  }

  template <class Output>
  operator Output()
  {
    Output out;
    ss_ >> out;
    if(ss_.bad() || ss_.fail())
      throw std::runtime_error("lexi_cast4 failed");

    return out;
  }

private:
  std::stringstream ss_;
};

// Overload for output type auto deduction.
template <class Input>
lexi_cast4_helper lexi_cast4(const Input& in)
{
  return lexi_cast4_helper(in);
}
// Overload for user-specified output type.
// Instead of returning conversion helper,
// this version returns output type.
template <class Output, class Input>
Output lexi_cast4(const Input& in)
{
  return lexi_cast4_helper(in);
}
  1. Sorry, Jeff. 明明是我自己愛玩卻還要遷拖到你. []
  2. 由於 IOStream 是不能 copy 的, 因此 lexi_cast3_helper 需要有 copy-ctor 才能 compile. 有趣的是, 因為 RVO 的關係, 這些 copy-ctor 根本沒有 run-time 的用處, 不會被執行到. 可以把它的內容 comment 掉, 甚至亂改, 只要能 compile & link, 仍舊會正常運作. []

« »