C++使用模板類實現任意類型switch和變量case

最近本身維護的一個項目program_options(是一個命令行生成與解析的C++庫)在實際應用的時候遇到一個需求:javascript

須要switch一個字符串來執行相應代碼塊,然而原生的switch-case條件選擇語法針對condition有嚴格的限制,下面摘錄一段switch的語法標準:java

switch statement

Transfers control to one of the several statements, depending on the value of a condition.linux

Syntax











attr(optional) switch ( condition ) statement


















attr(C++11) - any number of attributes
condition - any expression of integral or enumeration type, or of a class type contextually implicitly convertible to an integral or enumeration type, or a declaration of a single non-array variable of such type with a brace-or-equals initializer.
statement - any statement (typically a compound statement). case: and default: labels are permitted in statement and break; statement has special meaning.










attr(optional) case constant_expression : statement (1)

















attr(optional) default : statement (2)

















constant_expression - a constant expression of the same type as the type of condition after conversions and integral promotions


conditiongit

原生switch語法的condition支持整數,枚舉或者根據上下文可以隱式轉換爲整數或者枚舉的類,再或者是非數組類型的=或{}初始化語句,舉例來講就是以下四類:github

switch(100)
enum color {r, g, b}
switch(r)
switch(int n = 1)
switch(int n = {1})

caseexpress

必須是常量,這樣一來就沒法作變量與變量之間的比較。windows

statement數組

當statement沒有被{}包圍的時候,在其內使用聲明語句會致使編譯錯誤。好比:閉包

switch(num){
  case 1:
    int n = 0; // error: jump bypasses variable initialization
    break;
  default:
    break;
}

顯然個人需求不能用原生的switch來實現,得老老實實if-elseif-elseif...來分別判斷,致使代碼又長又臭,if越套越多,逐條判斷效率也讓人心塞。app

有需求就有想法,有想法就有創新,相信這樣的需求早就有人實現過了,好比:

想法很不錯,可是我想要更靈活的解決方案,我但願新的switch支持switch(object),case(object),還但願statement對變量聲明沒有限制,何不徹底拋開原生switch的枷鎖,本身利用標準庫造一個switch來解決問題呢。

藍圖

我但願利用C++強大的template來兼容任意類型,用C++11的lambda匿名函數實現statement,用操做符重載operator==來匹配條件,用hash表來提高匹配效率,看起來很容易不是嗎?

開始Coding以前我先擬定好藍圖:

// 藍圖1
select(condition, {
  {"case1", []() {
    // code goes here
  }},
  {"case2", []() {
    // code goes here
  }}
});
// 藍圖2
select(condition)
  .found("case1", []() {
    // code goes here
  })
  .found("case2", []() {
    // code goes here
  })
  .others([]() {
    // default
  });

我認可我是受到了javascript的影響,我一直覺得C++愈來愈像是一種高級的腳本語言,或許也是它將來的發展趨勢。

藍圖的設計首先符合C++的語法規範,沒有語法錯誤,其次力求語義明確,簡潔。

藍圖1的大括號太多,書寫時容易出錯。

藍圖2語法簡潔明瞭,我相信任何會閉包的Coder都能理解。

實現

有了藍圖後咱們就能夠照着這個模樣來寫代碼了,首先分析一下藍圖2。

  • 存在鏈式操做,顯然select函數要返回一個對象,該對象有found和others方法,而且,found方法要返回實例自己。

  • condition和found的第一個參數類型必須一致,但不必定是string,也能夠是int,Object,可用template實現

  • found第二個參數是lambda表達式,類型是std::function<...>,相似C裏面的函數指針,可定義爲回調函數。

  • 每一個found塊對應於switch裏的case,是一個kv關係,可用std::map來存儲關聯。

C++建議模板類的聲明和定義必須寫在同一個文件裏,所以起一個switch.hpp文件:

#include <functional>
#include <map>

template <typename Ty>
class Switch {
 public:
  Switch(){}

  explicit Switch(const Ty& target)
      : target_(target) {}

  Switch& found(const Ty& _case, const std::function<void(void)>& callback) {
    reflections_[_case] = callback;
    return *this;
  }
  
 private:
  const Ty& target_;
  std::map<const Ty, std::function<void(void)>> reflections_;
};

template <typename Ty>
Switch<Ty> select(const Ty& expression) {
  return Switch<Ty>(expression);
}

這麼一來就實現了found得鏈式操做,存儲了kv對,全局(也能夠在某個命名空間內)select函數是一個簡化書寫的幫助函數,建立對象後返回該對象的拷貝,實現了以下調用:

select(std::string("condition"))
  .found ...

接下來我須要實現查找到對應的target,而後調用它的callback。

增長一個done()方法,該方法被調用意味着結束整個Switch,開始匹配found塊,若是沒找到,調用others函數(對應default塊):

  inline void done() {
    auto kv = reflections_.find(target_);
    if (kv != reflections_.end()) {
      // found
      auto scope = kv->second;
      scope();
    } else if (has_others_scope_) {
      // not found, call others
      others_();
    }
  }

std::map的find方法時間複雜度是O(logN),而原生switch匹配時間複雜度是O(1),確定是有很大差距的,可是爲了實現switch沒有的功能,這點損失也是十分值得的。

others方法以下:

  inline void others(const Scope& callback) {
    has_others_scope_ = true;
    others_ = callback;
    this->done();
  }

當用戶調用others方法定義了default塊以後,就不必再調用done了,又能夠減小7個字符的書寫。

這裏has_others_scope_爲bool成員;others_是單獨存放的lambda表達式成員,爲了簡化查找,不宜放在reflections_中。

再簡化書寫,用typedef縮短類型,而後替換原類中相應類型爲短類型:

  typedef std::function<void(void)> Scope;
  typedef std::map<const Ty, Scope> Reflections;

這麼一來幾乎很完美了,全新的Switch以下:

  #define printl(line) printf((line)); printf("\n")
  std::string condition("windows");

  // match std::string
  select(condition)
    .found("apple", []() {
      printl("it's apple");
    })
    .found("windows", []() {
      printl("it's windows");
    })
    .found("linux", []() {
      printl("it's linux");
    }).done();

  // match int
  select(100)
    .found(10, []() {
      printl("it's 10");
    })
    .found(20, []() {
      printl("it's 20");
    })
    .others([]() {
      printl("nothing found");
    });

  // output
  // it's windows
  // nothing found

我想進一步實現自定義class的case,定義一個User類:

class User {
 public:
  explicit User(int age) : age_(age) {}

  bool operator<(const User& user) const { return this->age_ < user.age(); }

  int age() const { return age_; }

 private:
  int age_;
};

Switch以下:

  User u1(20), u2(22), ux(20);
  select(ux)
    .found(u1, []() {
      printl("it's u1");
    })
    .found(u2, []() {
      printl("it's u2");
    }).done();
  // it's u2

很是有必要說明的是這個重載:

bool operator<(const User& user) const { return this->age_ < user.age(); }

返回bool沒有問題,但爲何必須是operator<呢,緣由在這句:

auto kv = reflections_.find(target_);

std::map<>::find不是經過==進行查找的,而是<,所以必須重載<。

該重載必須被const修飾,緣由也是find這句裏面,const對象只能調用const方法。

標準庫裏的實現以下:

struct _LIBCPP_TYPE_VIS_ONLY less : binary_function<_Tp, _Tp, bool>
{
    _LIBCPP_CONSTEXPR_AFTER_CXX11 _LIBCPP_INLINE_VISIBILITY 
    bool operator()(const _Tp& __x, const _Tp& __y) const
        {return __x < __y;}
};

能夠很是明顯的看到const和<。

此外我還實現了Switch之間的found塊組合,比較簡單就不闡述了。

存在的問題

常量字符串的轉型問題:

select("condition")
  .found("case", ...)
  .done();

編譯器將"condition"理解爲const char[10],數組類型有固定長度,found塊的_case參數類型是const char[5],致使編譯錯誤。緣由在於:

Switch& found(const Ty& _case, const Scope& callback)

這裏傳遞const引用,所以編譯器把"case"當作了const char[5]。此時Ty的類型和說好的const char[10]不一致,編譯失敗。

解決方法是經過std::string來避免數組長度不匹配問題:

select(std::string("condition"))
  .found("case", ...)
  .done();

但願讀者有更好地解決方案。

完整代碼

這裏直接引用我項目裏面的實現:

#ifndef PROGRAM_OPTIONS_SWITCH_HPP_
#define PROGRAM_OPTIONS_SWITCH_HPP_

#include <functional>
#include <map>

namespace program_options {

/**
* @brief The Switch template class.
* @param Ty The target type.
*/
template <typename Ty>
class Switch {
 public:
  typedef std::function<void(void)> Scope;
  typedef std::map<const Ty, Scope> Reflections;

  Switch() : has_others_scope_(false) {}

  explicit Switch(const Ty& target)
      : target_(target), has_others_scope_(false) {}

  /**
   * @brief Create a case block with an expression and a callback function.
   * @param _case The case expression, variable is allowed.
   * @param callback The callback function, can be a lambda expression.
   * @return The current Switch instance.
   */
  Switch& found(const Ty& _case, const Scope& callback) {
    reflections_[_case] = callback;
    return *this;
  }

  /**
   * @brief Create a default block with a callback function,
   *        if no cases matched, this block will be called.
   * @param callback
   */
  inline void others(const Scope& callback) {
    has_others_scope_ = true;
    others_ = callback;
    this->done();
  }

  /**
   * @brief Finish the cases,
   * others() will call this method automatically.
   */
  inline void done() {
    auto kv = reflections_.find(target_);
    if (kv != reflections_.end()) {
      // found
      auto scope = kv->second;
      scope();
    } else if (has_others_scope_) {
      // not found, call others
      others_();
    }
  }

  /**
   * @brief Combine the cases to this Switch from another Switch.
   *        Note that this two Switch should be the same template.
   * @param _switch Another Switch instance.
   * @return
   */
  inline Switch& combine(const Switch& _switch) {
    for (auto kv : _switch.reflections()) {
      this->reflections_[kv.first] = kv.second;
    }
    return *this;
  }

  /**
   * @brief Return the case-callback pairs.
   * @return
   */
  inline Reflections reflections() const { return reflections_; }

 private:
  const Ty& target_;
  bool has_others_scope_;
  Scope others_;
  Reflections reflections_;
};

/**
 * @brief Define which expression does the Switch match.
 * @param expression
 * @return
 */
template <typename Ty>
Switch<Ty> select(const Ty& expression) {
  return Switch<Ty>(expression);
}
}

#endif  // PROGRAM_OPTIONS_SWITCH_HPP_

歡迎各位讀者指正。

相關文章
相關標籤/搜索