閱讀971 返回首頁    go 阿裏雲 go 技術社區[雲棲]


C++11:使用 auto/decltype/result_of使代碼可讀易維護

C++11 終於加入了自動類型推導。以前,我們不得不使用Boost的相關組件來實現,現在,我們可以使用“原生態”的自動類型推導了!

C++引入自動的類型推導,並不是在向動態語言(強類型語言又稱靜態類型語言,是指需要進行變量/對象類型聲明的語言,一般情況下需要編譯執行。例如C/C++/Java;弱類型語言又稱動態類型語言,是指不需要進行變量/對象類型聲明的語言,一般情況下不需要編譯(但也有編譯型的)。例如PHP/ASP/Ruby/Python/Perl/ABAP/SQL/JavaScript/Unix Shell等等)靠攏,通過弱化類型實現編程的靈活性;而是在保持類型安全的前提下提高代碼的可讀性,易用性和通用性。要理解這點就必須對C++泛型編程(GP)和為什麼要泛型有一定認識,推薦閱讀:劉未鵬:泛型編程:源起、實現與意義。類型自動推導帶來的最大好處就是擁有初始化表達式的負責類型聲明簡化了。很多時候,名字空間,模版成了類型的一部分,經常要寫很長的表達式,不小心寫錯了,編譯器就給爆出一堆近似與亂碼的錯誤信息,調試起來更是頭疼。

1) auto

簡單用法:

map< int, map<int,int> > m;
// C++98/03 style:
map<int, map<int,int> >::const_iterator it = m.begin();
// C++11 style
const auto it = m.begin(); 
其實,我們隻需要知道it是迭代器就行,通過它可以訪問容器的元素。而且如果要修改m的類型,那麼導致大量的迭代器都要修改,違反了DRY(Don't Repeat Yourself,不要重複粘帖你的代碼)原則。即使使用typedef,也不能完全避免這個問題。所以,任何人都應該使用auto!(注:auto的語義已經更改,C++98/03是修飾自動存儲期的局部變量。但是實際上這個關鍵字幾乎沒有人用,因為一般函數內沒有聲明為static的變量都是自動存儲期的局部變量)

本文講詳細討論auto/decltype/result_of 用法及應用場景。

auto並不是說這個變量的類型不定,或者在運行時再確定,而是說這個變量在編譯時可以由編譯器推導出來,使用auto和decltype隻是占位符的作用,告訴編譯器這個變量的類型需要編譯器推導,借以消除C++變量定義和使用時的類型冗餘,從而解放了程序員打更多無意義的字符,避免了手動推導某個變量的類型,甚至有時候需要很多額外的開銷才能繞過的類型聲明。

但是,auto不能解決精度問題:

auto a = numeric_limits<unsinged int>::max();
auto b = 1;
auto c = a + b;// c is also unsigned int, and it is 0 since it has overflowed.
這並不像一些動態語言那樣,會自動擴展c以存儲更大的值。因此這點要注意。

auto的使用細則:

int x;
int *ptr = &y;
double foo();
int &bar();

auto *a = &x; // int *
auto &b = x; // int &
auto c = ptr; //int *
auto &d = ptr; // int *
auto *e = &foo(); // compiler error, the pointer cannot point to a temporary variable.
auto &f = foo(); // compiler error
auto g = bar(); // int
auto &h = bar(); // int &
變量a, c , d都是指針,且都指向x,實際上對於a,c,d三個變量而言,聲明其為auto *或者auto並沒有區別。但是,如果變量要是另外一個變量的引用,則必須使用auto &,注意g和h的區別。

auto和const,volatile和存在這一定的關係。C++將volatile和const成為cv-qualifier。鑒於cv限製符的特殊性,C++11標準規定auto可以和cv-qualifier一切使用,但是auto聲明的變量並不能從其初始化表達式中帶走cv-qualifier。還是通過實例理解吧:

double x;
float * bar();

const auto a = foo(); // const double
const auto &b = x; // const double &
volatile auto *c = bar(); // volatile float *

auto d = a; // double
auto &e = a; // const double &
auto f = c; // float *
volatile auto &g = c; // volatile float * &
注意auto聲明的變量d,f都無法帶走a 和f的const和volatile。但是例外是引用和指針,聲明的變量e和g都保持了其引用對象相同的屬性。

2) decltype

decltype主要為庫作者所用,但是如果是我們需要用template,那麼使用它也能簡潔我們的代碼。

與C完全不支持動態類型的是,C++在C++98標準中就部分支持動態類型了:RTTI(RunTime Type Identification)。RTTI就是為每個類型產生一個type_info的數據,我們可以使用typeid(var)來獲取一個變量的type_info。type_info::name就是類型的名字;type_info::hash_code()是C++11中新增的,返回該類型唯一的hash值。

在decltype產生之前,很多編譯器廠商都有自己的類型推導的擴展,比如GCC的typeof操作符。

言歸正傳,decltype就是不用計算表達式就可以推導出表達式所得值的類型。

template<typename T1, typename T2>
void sum(T1 &t1, T2 &t2, decltype(t1 + t2) &s){
  s = t1 + t2;
}

// another scenario

template<typename T1, typename T2>
auto sum(T1 &t1, T2 &t2) ->decltype(t1 + t2)){
  return t1 + t2;
}
很容易看出與auto的不同。實例化模版的時候,decltype也可以有用武之地:
int hash(char *);

map<char *, decltype(hash(nullptr))> m;// map<char *, int> m may be more simple, but when hash value changed to other type, such as 
//string, it would cause a lot of maintenance effort. 

接下來看一個更複雜的例子。首先定義Person:

struct Person
{
  string name;
  int age;
  string city;
};

我們想得到一係列的multimap,可以按照city,age進行分組。
第一個版本:

template<typename T, typename Fn>
multimap<T, Person> GroupBy(const vector<Person>& vt, const Fn& keySlector)
{
  multimap<T, Person> map;
  std::for_each(vt.begin(), vt.end(), [&map](const Person& person)
  {
    map.insert(make_pair(keySlector(person), person)); //keySlector返回值就是鍵值,通過keySelector擦除了類型
  });

return map;
}

通過傳入key type,和獲取相應值的函數(可以使用lambda),就可以獲取這個multimap。但是,實際上key type就是Fn的返回值,可以不用傳入:通過keySlector(person)進行判斷。這裏就要說說如何獲取閉包的返回值類型了。獲取閉包的返回值類型的方法有三種:

  1.     通過decltype
  2.     通過declval
  3.     通過result_of


第一種方式,通過decltype:

multimap<decltype(keySlector((Person&)nulltype)), Person>或者multimap<decltype(keySlector(*((Person*)0))), Person>
這種方式可以解決問題,但不夠好,因為它有兩個magic number:nulltype和0。
通過declval:
multimap<decltype(declval(Fn)(declval(Person))), Person>
這種方式用到了declval,declval的強大之處在於它能獲取任何類型的右值引用,而不管它是不是有默認構造函數,因此我們通過declval(Fn)獲得了function的右值引用,然後再調用形參declval(Person)的右值引用,需要注意的是declval獲取的右值引用不能用於求值,因此我們需要用decltype來推斷出最終的返回值。這種方式比剛才那種方式要好一點,因為消除了魔法數,但是感覺稍微有點麻煩,寫的代碼有點繁瑣,有更好的方式嗎?看第三種方式吧:

通過result_of
multimap<typename std::result_of<Fn(Person)>::type, Person>
std::result_of<Fn(Arg)>::type可以獲取function的返回值,沒有魔法數,也沒有declval繁瑣的寫法,很優雅。其實,查看源碼就知道result_of內部就是通過declval實現的,作法和方式二一樣,隻是簡化了寫法。

最終版本:

vector<Person> v = { {"aa", 20, "shanghai"}, { "bb", 25, "beijing" }, { "cc", 25, "nanjing" }, { "dd", 20, "nanjing" } };
typedef typename vector<Persion>::value_type value_type;
template<typename Fn>
multimap<typename result_of<Fn(value_type)>::type, value_type> groupby(const vector<value_type> &v, const Fn& f)  // -> decltype(f(*((value_type*)0))),f((value_type&)nullptr)
{
//typedef typename result_of<Fn(value_type)>::type ketype;
  typedef  decltype(declval<Fn>()(declval <value_type>())) ketype;

  multimap<ketype, value_type> mymap;
  std::for_each(begin(v), end(v), [&mymap, &f](value_type item)
  {
    mymap.insert(make_pair(f(item), item));
  });
  return mymap;
}

看一下最終的調用情況:

vector<Person> v = { {"aa", 20, "shanghai"}, { "bb", 25, "beijing" }, { "cc", 25, "nanjing" }, { "dd", 20, "nanjing" } };
// group by age
auto r1 = range.groupby([](const Person& person){return person.age; });
// group by name
auto r2 = range.groupby([](const Person& person){return person.name; });
// group by city
auto r3 = range.groupby([](const Person& person){return person.city; });

result_of 其實就是通過decltype來推導函數的返回類型。result_of的一種可能的實現如下:

template<class F, class... ArgTypes>
struct result_of<F(ArgTypes...)>
{
  typedef decltype(
                  declval<F>()(declval<ArgTypes>()...)
                  ) type;
}

另外一個使用result_of的例子:
template< class Obj >
class CalculusVer2 {
public:
    template<class Arg>
    typename std::result_of<Obj(Arg)>::type operator()(Arg& a) const
    { 
        return member(a);
    }
private:
    Obj member;
};

總結:

auto適用於任何人,除非需要類型轉換,否則你應該使用它

decltype適合推導表達式,因此在庫中大量使用,當然它也可以推導函數的返回值,但是函數的返回值的推導,還是交給result_of吧!

引用:

https://www.cnblogs.com/qicosmos/p/3286057.html

最後更新:2017-04-03 12:53:51

  上一篇:go C++編程規範之27:使用算術操作符和賦值操作符的標準形式
  下一篇:go 雷軍:小米不能說的秘密---不成功都很難