用 C + + 11进行重构

鉴于 c + + 提供的新工具集,许多程序员,旨在简化代码,表现力,效率,浏览他们的旧代码,并作出微调(一些无意义的,一些成功的) ,以实现他们的目标。尽量不要在这些工作上浪费太多的时间,只是做一些非侵入性的、自我包含的改变,最佳做法是什么?

让我把这个显而易见的事情划掉:

  • 使用 奥托运行基于迭代器的循环:

    for (std::vector<foo>::const_iterator it(lala.begin()), ite(lala.end()); it != ite;
    ++it);
    // becomes
    for (auto it(lala.cbegin()), ite(lala.cend()); it != ite; ++it);
    
  • Use tie for multiple assignments that just produce C-style rows of code ( how to assign multiple values into a struct at once? )

    a = 1;
    b = 2;
    c = 3;
    d = 4;
    e = 5;
    // becomes
    std::tie(a, b, c, d, e) = std::make_tuple(1, 2, 3, 4, 5);
    
  • To make a class non inheritable just declare it as "final" and delete the code that achieved such a behavior http://www.parashift.com/c++-faq/final-classes.html

  • Use the delete keyword to explicitly hide constructors/destructors instead of declaring them private (eg code to create heap based objects, non copyable objects etc)

  • Turn trivial functors created just to facillitate the execution of a single STL algorithm into lambda functions (apart from reducing code cluttering you'll have guaranteed inlined calls)

  • Simplify RAII wrapping of an object by just using a smart pointer

  • Get rid of bind1st, bind2nd and just use bind

  • Replace hand written code for type traits (Is_ptr_but_dont_call_for_const_ptrs<> and such :) ) with standard code provided by < type_traits >

  • Stop including boost headers for functionallity now implented in STL (BOOST_STATIC_ASSERT vs static_assert)

  • Provide move semantics to classes (although this wouldn't qualify as a dirty/quick/easy change)

  • Use nullptr where possible instead of the NULL macro and get rid of the code that filled containers of pointers with 0's casted to object type

    std::vector<foo*> f(23);
    for (std::size_t i(0); i < 23; ++i)
    { f[i] = static_cast<foo*>(0); }
    // becomes
    std::vector<foo*> f(23, nullptr);
    
  • Clear the vector data accessing syntax

    std::vector<int> vec;
    &vec[0];    // access data as a C-style array
    vec.data(); // new way of saying the above
    
  • Replace throw() with noexcept (apart from avoiding the deprecated exception specifiation you get some speed benefits http://channel9.msdn.com/Events/GoingNative/2013/An-Effective-Cpp11-14-Sampler @ 00.29.42)

    void some_func() noexcept; // more  optimization options
    void some_func() throw();  // fewer optimization options
    void some_func() ;         // fewer optimization options
    
  • Replace code where you'd push a tempory in a container and hoped that the optimizer would ellide the copy away, with an "emplace" function where available, in order to perfectly forward the argument and construct directly an object into a container without temporary at all.

    vecOfPoints.push_back(Point(x,y,z)); // so '03
    vecOfPoints.emplace_back(x, y, z);   // no copy or move operations performed
    

UPDATE

The answer by Shafik Yaghmour was rightfully awarded the bounty for having the greatest acceptance by the audience.

The answer by R Sahu was my accepted one, because the combination of features it proposes captures the spirit of refactoring : making code clearer and cleaner and simpler and elegant.

6379 次浏览

For-each 语法:

std::vector<int> container;


for (auto const & i : container)
std::cout << i << std::endl;

1. 更换兰特

C + + 11最大的收获之一就是使用 随机报头随机报头中的所有选项来替代使用 rand()。在许多情况下,替换 rand()应该是直截了当的。

斯蒂芬 · T · 拉瓦维奇(Stephan T. Lavavej)可能在他的演讲 被认为是有害的中强调了这一点。示例显示了使用 rand()的来自 [0,10]的统一整数分布:

#include <cstdlib>
#include <iostream>
#include <ctime>


int main()
{
srand(time(0)) ;


for (int n = 0; n < 10; ++n)
{
std::cout << (rand() / (RAND_MAX / (10 + 1) + 1)) << ", " ;
}
std::cout << std::endl ;
}

使用 Std: : uniform _ int _ distrubution:

#include <iostream>
#include <random>


int main()
{
std::random_device rd;


std::mt19937 e2(rd());
std::uniform_int_distribution<> dist(0, 10);


for (int n = 0; n < 10; ++n) {
std::cout << dist(e2) << ", " ;
}
std::cout << std::endl ;
}

随之而来的应该是从 : Random _ shuffle移动到 洗牌,这就是为 贬低兰德和朋友所做的努力。这是最近在 SO 问题 为什么在 C + + 14中不推荐使用 std: : shuffle 方法?中提到的。

注意,不能保证这些分布是 跨平台的一致性

2. 使用 std: : to _ string 代替 std: : ostringstream 或 sprintf

C + + 11提供了 To _ string,它可以用来将数字转换成 字符串,它将产生相当于 Sprintf的内容。最有可能的是,这将用于代替 Ostringstreamsnprintf。这更方便,可能没有太大的性能差异,我们可以从 C + + 中的快速整数到字符串转换的文章中看到,如果性能是主要问题,可能有更快的替代方案:

#include <iostream>
#include <sstream>
#include <string>


int main()
{
std::ostringstream mystream;
mystream << 100 ;
std::string s = mystream.str();


std::cout << s << std::endl ;


char buff[12] = {0};
sprintf(buff, "%d", 100);
std::string s2( buff ) ;
std::cout << s2 << std::endl ;


std::cout << std::to_string( 100 ) << std::endl ;
}

3. 使用 Constexpr 代替模板元编程

如果您处理的是文字,那么在某些情况下,使用 conexpr 函数而不是模板元编程可能会产生更清晰的代码,并且可能会编译得更快。想要速度? 使用 conexpr 元编程!文章提供了一个使用模板元编程确定素数的例子:

struct false_type
{
typedef false_type type;
enum { value = 0 };
};


struct true_type
{
typedef true_type type;
enum { value = 1 };
};


template<bool condition, class T, class U>
struct if_
{
typedef U type;
};


template <class T, class U>
struct if_<true, T, U>
{
typedef T type;
};


template<size_t N, size_t c>
struct is_prime_impl
{
typedef typename if_<(c*c > N),
true_type,
typename if_<(N % c == 0),
false_type,
is_prime_impl<N, c+1> >::type >::type type;
enum { value = type::value };
};


template<size_t N>
struct is_prime
{
enum { value = is_prime_impl<N, 2>::type::value };
};


template <>
struct is_prime<0>
{
enum { value = 0 };
};


template <>
struct is_prime<1>
{
enum { value = 0 };
};

并使用 conexpr 函数:

constexpr bool is_prime_recursive(size_t number, size_t c)
{
return (c*c > number) ? true :
(number % c == 0) ? false :
is_prime_recursive(number, c+1);
}


constexpr bool is_prime_func(size_t number)
{
return (number <= 1) ? false : is_prime_recursive(number, 2);
}

Constexpr 版本更短,更容易理解,并且显然比模板元编程实现执行得更好。

4. 使用类成员初始化提供默认值

正如最近在 声明中新的 C + + 11成员初始化特性使得初始化列表过时了吗?类成员初始化中介绍的那样,可以用来提供默认值,并且可以简化类具有多个构造函数的情况。

比雅尼·斯特劳斯特鲁普 为 C + + 11常见问题解答提供了一个很好的例子,他说:

这样可以节省一点输入,但真正的好处来自于具有多个构造函数的类。通常,所有构造函数都为成员使用公共初始值设定项:

并提供具有通用初始值设定项的成员示例:

class A {
public:
A(): a(7), b(5), hash_algorithm("MD5"), s("Constructor run") {}
A(int a_val) : a(a_val), b(5), hash_algorithm("MD5"), s("Constructor run") {}
A(D d) : a(7), b(g(d)), hash_algorithm("MD5"), s("Constructor run") {}
int a, b;
private:
HashingFunction hash_algorithm;  // Cryptographic hash to be applied to all A instances
std::string s;                   // String indicating state in object lifecycle
};

说:

哈希算法和 s 各有一个默认值,这个事实在代码混乱中丢失了,在维护过程中很容易成为问题。相反,我们可以剔除数据成员的初始化:

class A {
public:
A(): a(7), b(5) {}
A(int a_val) : a(a_val), b(5) {}
A(D d) : a(7), b(g(d)) {}
int a, b;
private:
HashingFunction hash_algorithm{"MD5"};  // Cryptographic hash to be applied to all A instances
std::string s{"Constructor run"};       // String indicating state in object lifecycle
};

注意,在 C + + 11中,类成员初始值设定项中使用的类是 不再是一个集合,尽管这个限制在 C + + 14中已经取消。

5. 使用来自 cstdint 的固定宽度整数类型,而不是手工滚动的 typedef

由于 C + + 11标准使用 C99作为标准参考,我们也得到了 固定宽度整数类型。例如:

int8_t
int16_t
int32_t
int64_t
intptr_t

虽然其中一些是可选的,但是对于确切的宽度整数类型,C99节 7.18.1.1中的以下内容适用:

这些类型是可选的。但是,如果实现提供宽度为8的整数类型, 16、32或64位,没有填充位,并且(对于有符号类型) 有一个二的补数表示,它应该定义 相应的 typedef 名称。

特写: 让开

“明确区分复制和移动资源”

std::string tmp("move");
std::vector<std::string> v;
v.push_back(std::move(tmp));
//At this point tmp still be the valid object but in unspecified state as
// its resources has been moved and now stored in vector container.

使用 < a href = “ http://www.stroustrup.com/C + + 11FAQ.html # united-init”rel = “ nofollow noReferrer”> 统一初始化语法 变量初始化变量初始化

widget w(x); // old
widget w{x}; // new

避免像 C + + 最烦人的解析这样的问题(Herb Sutter 在链接文章中解释了为什么这种新方法更好的其他原因)

这篇博客文章建议,如果所有的所有权成为一个类遵循 RAII 原则的 零法则,允许摆脱 C + + 11中的三/四/五规则。

然而,Scott Meyers 向 给你展示,如果不明确地编写析构函数、复制/移动构造函数和赋值操作符,那么如果稍微更改代码(比如用于调试) ,就会产生一些细微的问题。然后,他建议显式声明 违约(C + + 11特性)这些函数:

~MyClass()                           = default;
MyClass( const MyClass& )            = default;
MyClass( MyClass&& )                 = default;
MyClass& operator=( const MyClass& ) = default;
MyClass& operator=( MyClass&& )      = default;

我将向列表中添加委托构造函数和类内成员初始化器。

通过使用委托构造函数和类内初始化进行简化

使用 C + + 03:

class A
{
public:


// The default constructor as well as the copy constructor need to
// initialize some of the members almost the same and call init() to
// finish construction.
A(double data) : id_(0), name_(), data_(data) {init();}
A(A const& copy) : id_(0), name_(), data_(copy.data_) {init();}


void init()
{
id_ = getNextID();
name_ = getDefaultName();
}


int id_;
string name_;
double data_;
};

使用 C + + 11:

class A
{
public:


// With delegating constructor, the copy constructor can
// reuse this constructor and avoid repetitive code.
// In-line initialization takes care of initializing the members.
A(double data) : data_(data) {}


A(A const& copy) : A(copy.data_) {}


int id_ = getNextID();
string name_ = getDefaultName();
double data_;
};

使用 Constexpr 优化简单的数学函数,特别是当它们被称为内部循环时。这将允许编译器在编译时计算它们,从而节省您的时间

例子

constexpr int fibonacci(int i) {
return i==0 ? 0 : (i==1 ? 1 : fibonacci(i-1) + fibonacci(i-2));
}

另一个例子是使用 std::enable_if来限制特定模板函数/类中允许的模板参数类型。这将使您的代码更加安全(如果您还没有使用 SFINAE 来约束旧代码中可能的模板参数) ,当您隐式假定模板类型的某些属性时,这仅仅是一行额外的代码

例如:

template
<
typename T,
std::enable_if< std::is_abstract<T>::value == false, bool>::type = false // extra line
>
void f(T t)
{
// do something that depends on the fact that std::is_abstract<T>::value == false
}

更新1: 如果你有一个在编译时大小已知的小数组,并且你想要避免 std: : Vector 中堆分配的开销(意思是: 你想要堆栈上的数组) ,你在 C + + 03中唯一的选择就是使用 c 样式的数组。改成 std::array。这是一个简单的更改,它为您提供了 std: : Vector + 堆栈分配中的大量功能(如前所述,比堆分配快得多)。

使用智能指针。请注意,在某些情况下仍然有很好的理由使用裸指针,检查指针是否应该智能的最佳方法是查找指针上 delete的用法。

也没有理由使用 new。用 make_sharedmake_unique代替每个 new

不幸的是,最好的解决方案 IMO 是自己实现它(请参阅以上连结) ,并放置一些宏来检查 __cplusplus版本(make_unique在 C + + 14中可用)。

使用 make_uniquemake_shared对于保证代码异常安全非常重要。

  1. std::map改为 std::unordered_map,将 std::set改为 std::unordered_set在容器元件的顺序不相关的情况下,极大地提高了性能。
  2. 当您希望避免非自愿插入时,可以使用 std::map::at而不是使用方括号语法插入。
  3. 如果要使用 typedef模板,请使用别名模板。
  4. 使用初始化列表而不是 for 循环来初始化 STL 容器。
  5. 用 std: : array 替换固定大小的 C 数组。
  1. 与非作用域枚举相比,更喜欢作用域枚举

    • 在 C + + 98中,枚举没有作用域,如下面的代码片段所示。这种枚举器的名称属于包含枚举的范围,即该范围中的其他任何内容都不能具有相同的名称。

      enum Color{ blue, green, yellow };
      bool blue = false;    // error: 'blue' redefinition
      

      但是,在 C + + 11中,scoped enums可以解决这个问题。

      enum class Color{ blue, green, yellow };
      bool blue = false;     // fine, no other `blue` in scope
      Color cc = blue;       // error! no enumerator `blue` in this scope
      Color cc = Color::blue; // fine
      auto c = Color::blue;  // fine
      
    • The enumerators of scope enums are more strongly typed. But, the enumerators of unscoped enums implicitly convert to other types

      enum Color{ blue, green, yellow };
      std::vector<std::size_t> getVector(std::size_t x);
      Color c = blue;
      
      
      if (c < 10.1) {             // compare Color with double !!
      auto vec = getVector(c); // could be fine !!
      }
      

      但是,在这种情况下,scoped enums将失败。

      enum class Color{ blue, green, yellow };
      std::vector<std::size_t> getVector(std::size_t x);
      Color c = Color::blue;
      
      
      if (c < 10.1) {             // error !
      auto vec = getVector(c); // error !!
      }
      

      通过 static_cast修好它

      if (static_cast<double>(c) < 10.1) {
      auto vec = getVector(static_cast<std::size_t>(c));
      }
      
    • unscoped enums may be forward-declared.

      enum Color;          // error!!
      enum class Color;    // fine
      
    • Both scoped and unscoped enums support specification of the underlying type. The default underlying type for scoped enums is int. Unscoped enums have no default underlying type.

  2. Using Concurrency API

    • Prefer task-based to thread-based

      If you want to run a function doAsyncWork asynchronously, you have two basic choices. One is thread-based

      int doAsyncWork();
      std::thread t(doAsyncWork);
      

      另一个是 基于任务的

      auto fut = std::async(doAsyncWork);
      

      显然,我们可以通过 基于任务的基于线程的更容易地得到 doAsyncWork的返回值。使用 task-based方法很简单,因为从 std::async返回的将来提供了 get 函数。如果 doAsyncWork发生异常,那么 get函数就更重要了,因为 get也提供了对异常的访问。

    • Thread-based要求手动管理线程耗尽、超额订阅、负载平衡和适应新平台。 但是使用默认启动策略的 Task-based via std::async没有这些缺点

    这里有几个链接:

    C + + 中的并发

    面向并行和并发的 C/C + + 编程抽象

使用 重写关键字

将派生类中的虚函数标记为重写(当然,如果它们确实重写的话)。这可以防止将来引入错误,例如通过更改基类中虚函数的签名,而忘记相应地更改所有派生类中的签名。