使用C++ 11进行重构

75

鉴于C++提供的新工具集,许多程序员旨在简化代码、增加表现力和效率,浏览其旧代码并进行一些微调(有些没有意义,有些成功)以实现目标。在尽量不浪费太多时间的情况下,只进行非侵入式和自包含的更改,有哪些最佳实践呢?

让我划掉显而易见的:

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

  • 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);
    
  • 使用tie进行多个赋值,仅需生成 C 风格的代码行(如何一次性向结构体赋多个值?

  • 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);
    
  • 要使类不可继承,只需将其声明为 "final" 并删除实现此行为的代码 http://www.parashift.com/c++-faq/final-classes.html

  • 使用 delete 关键字显式隐藏构造函数/析构函数,而不是将它们声明为私有(例如创建基于堆的对象、不可复制的对象等)

  • 将仅用于方便执行单个 STL 算法的普通函数对象改为lambda函数(除了减少代码混乱外还可获得内联调用的保证)

  • 通过 智能指针简化对象的 RAII 封装

  • 摒弃 bind1st、bind2nd,并使用 bind

  • 使用 <type_traits> 提供的标准代码替换手写的类型特性代码(Is_ptr_but_dont_call_for_const_ptrs <>之类的 :))

  • 停止包含已在 STL 中实现的功能的 boost 头文件(BOOST_STATIC_ASSERT 与 static_assert)

  • 为类提供移动语义(虽然这不算是一个不经意间、快速、简单的更改)

  • 在可能的情况下使用 nullptr 替代 NULL 宏,并且摆脱填充指针容器为对象类型转换为 0 的代码

  • 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);
    
  • 清除向量数据访问语法

  • std::vector<int> vec;
    &vec[0];    // access data as a C-style array
    vec.data(); // new way of saying the above
    
  • 将 throw() 替换为 noexcept (除了避免使用已弃用的异常规范外,还可以获得一些速度优势。详情请参见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
    
  • 如果可用,替换在容器中推送临时对象并希望优化器消除复制的代码,使用 "emplace" 函数,以便完全转发参数并直接将对象构造到容器中,而不需要任何临时对象。

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

更新

Shafik Yaghmour的答案 应当被认为是受众接受度最高的,因此获得了悬赏。

R Sahu的答案 是我所选的,因为它提出的 综合方案 捕捉到了重构的精髓:使代码更加清晰、简洁、优雅。


23
请不要关闭这个。它真的很有用。 - Karoly Horvath
2
我不认为这是“基本上基于意见的”问题。绝对不是。然而,这是一种大型列表类型的问题,也不太适合Stack Overflow的格式。 - Konrad Rudolph
2
可能是什么C++惯用法在C++11中已被弃用的重复问题。 - TemplateRex
4
如果你想修改内部数据,使用.data()而不是&container[0]有个限制。这对于std::string是不起作用的,因为.data()对于std::string来说与.c_str()相同,返回一个常量指针。另外,在MSVC2013中,push_back使用T&&,等同于emplace_back - Brandon
5
使用 override 关键字来表明一个函数是覆盖了基类中的虚函数,而不是引入了一个新的函数或者隐藏了基类中的函数。此外,我建议不要将每个类都声明为 final。应该谨慎使用,因为它会使得测试代码比必要的更加繁琐。 - sdkljhdf hda
显示剩余4条评论
11个回答

31

1. 替换rand函数

C++11的一个重大进展就是使用random header中提供的所有选项替换rand()函数。在许多情况下,替换rand()应该是很简单的。

Stephan T. Lavavej在他的演示文稿rand() Considered Harmful中可能是最强烈地表达了这一点。示例展示了如何使用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_distribution

#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 ;
}

随着Deprecate rand and Friends的努力,我们应该从std::random_shuffle转移到std::shuffle。最近在SO问题Why are std::shuffle methods being deprecated in C++14?中也有相关讨论。
请注意,分布在不同平台上并不保证consistent across platforms2. 使用std::to_string而不是std::ostringstream或sprintf C++11提供了std::to_string,可用于将数字转换为std::string,它会生成与等效的std::sprintf内容。最有可能使用它来替代std::ostringstreamsnprintf。这更多是一种方便,性能差异可能不大,我们可以从Fast integer to string conversion in 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代替模板元编程

如果你正在处理字面量,使用constexpr函数而不是模板元编程可能会产生更清晰的代码,并且可能编译得更快。文章想要速度?使用constexpr元编程!提供了一个使用模板元编程确定质数的示例:

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 };
};

并使用constexpr函数:

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成员初始化特性是否已经使初始化列表过时?中介绍的那样,可以使用类成员初始化来提供默认值,并且可以简化具有多个构造函数的类的情况。

Bjarne Stroustrup在C++11 FAQ中提供了一个很好的例子,他说:

这可以节省一些打字,但真正的好处在于具有多个构造函数的类。通常,所有构造函数都使用成员的公共初始化器:

并提供了具有公共初始化器的成员的示例:

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名称。”

1
一开始看起来有点过度,但是在观看演示后,我承认有很多问题我之前并不知道。这是一个伟大的代码改进。 - Nikos Athanasiou
@NikosAthanasiou,考虑到您的悬赏,我的回答是否没有提供足够的细节?如果是这样,您想要看到哪些细节? - Shafik Yaghmour
赏金是为了激励更多的答案和技巧被揭示出来,并且让更多的人评论他们使用过的方法以及有效与否。你的回答既详尽又有用;我认为它不需要改进,从目前的情况来看,它将保持最受欢迎的状态并赢得赏金。 - Nikos Athanasiou
@Nikos Athanasiou 完全同意。我喜欢他的回答。通常当这种情况发生时(你有另一个例子来补充和/或升级答案),我会给出引用,比如“为了补充 Vin 给出的先前答案,这里是...”。但不要忘记,在 C++11 之前,像斐波那契计算、pow 或小数字都是使用模板元编程完成的,所以我们的答案并没有那么不同。 - Vivian Miranda

20

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

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

在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_;
};

12

for-each 语法:

std::vector<int> container;

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

2
我不明白为什么人们仍然更喜欢基于迭代器的for循环,使用lambda的for_each等。基于范围的for循环可以减少代码量,并且显然更易于阅读。 - Murat Şeker
1
@MuratŞeker:当你有两个迭代器和两个大小相等的容器时,这就容易多了。 - MSalters
2
除此之外,我认为没有理由不使用迭代器。当您必须在遍历容器时修改它时,您不能使用基于范围的迭代。迭代器通常提供更多控制。 - voodooattack
1
在https://dev59.com/qXzaa4cB1Zd3GeqPKwkv中提到了使用范围for循环的两个缺点。话虽如此,对于非复杂循环来说,使用“新”的方式确实是一个好习惯。 - Nikos Athanasiou
顺便提一下,std::endl 会导致控制台缓冲区刷新。通常使用 "\n" 会更快。 - gradbot

11
  1. 将元素的顺序无关的 std::map 改为 std::unordered_map,将 std::set 改为 std::unordered_set,可以显著提高性能。
  2. 在避免非自愿插入的情况下,使用 std::map::at 而不是方括号语法插入。
  3. 当您想要 typedef 模板时,使用别名模板。
  4. 使用初始化列表而非 for 循环来初始化 STL 容器。
  5. 用 std::array 替换固定大小的 C 数组。

5
这里有一个打字错误,正确应为 std::unordered_set。这种更改只能增强足够大的映射和集合的性能(否则只会降低程序的效率)。此外,在对这些容器进行迭代时可能会降低性能。 - Constructor

10

6
除非“widget”具有接受“std::initializer_list”的构造函数,否则这是一个好主意。 - Casey

9
本博客文章提出了“零规则”,如果类的所有所有权遵循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;

7
功能:std::move “明确表达复制和移动资源之间的明显区别。”
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.

2
移动后,不能保证tmp处于“空状态”,只能保证它处于“有效但未指定”的状态。(实际上,具有小对象优化的高质量字符串实现可能会使tmp保持不变。) - Casey

6
  1. Prefer scoped enums to unscoped enums

    • In C++98 the enums, there is no scoped for enums like the following code snippet. The names of such enumerators belong to the scope containing enum, namely nothing else in that scope may have the same name.

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

      However, in C++11, the scoped enums can fix this issue. scoped enum are declared var enum class.

      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 !!
      }
      

      However, scoped enums will be failed in this case.

      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 !!
      }
      

      Fix it through 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);
      

      The other is task-based.

      auto fut = std::async(doAsyncWork);
      

      Obviously, we can get the return value of doAsyncWork through task-based more easily than thread-based. With the task-based approach, it’s easy, because the future returned from std::async offers the get function. The get function is even more important if doAsyncWork emits an exception, because get provides access to that, too.

    • Thread-based calls for manual management of thread exhaustion, oversubscription, load balancing, and adaptation to new platforms. But Task-based via std::async with the default launch policy suffers from none of these drawbacks.

    Here are several links:

    Concurrency In C++

    C/C++ Programming Abstractions for Parallelism and Concurrency


您可能希望提及作用域枚举的其他好处。此外,讨论关于并发性在C++11之前存在哪些替代方案也会很有帮助。 - Shafik Yaghmour

5

使用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-style 数组。现在请将其改为 std::array。这是一个简单的更改,提供了大量 std::vector 中存在的功能和栈分配(比堆分配快得多,如前所述)。


2
不要用这种方式计算斐波那契数。 - Constructor
1
不确定为什么,特别是对于N不是很大的F_N(对于大的N可能有更优化的公式)。但这只是证明了您可以使用constexpr做什么的原则。我已经看到人们提倡使用constexpr版本的小整数pow、log和sqrt,带有一些注意事项。如果您可以在编译时执行在内部循环中调用的函数,则是良好的优化。 - Vivian Miranda

4
使用智能指针。请注意,在某些情况下仍有使用裸指针的好理由,检查指针是否应该是智能的最好方法是查找其上的delete用途。
也没有理由使用new。将每个new替换为make_sharedmake_unique
不幸的是,make_unique没有被纳入C++11标准,我认为最好的解决方案是自己实现它(见上一个链接),并放置一些宏来检查__cplusplus版本(make_unique在C++14中可用)。
使用make_uniquemake_shared非常重要,以使您的代码具有异常安全性。

直接分配给智能指针与使用那些制造函数一样安全。如果智能指针构造函数抛出异常,它会删除已构造的对象。但是,使用自定义分配器+释放器可以优化make_shared,因此共享指针的记账信息被预置到对象之前。 - Deduplicator
直接分配给智能指针与使用那些制造函数一样安全。但如果您将智能指针作为参数传递给另一个函数,则不是这样的。请参见此链接 - sbabbi
你的意思是,如果在构造函数中将智能指针作为参数传递给一个函数,并且你至少传递了另外一个可能会抛出异常的参数,对吗?看起来我们两个的评论都需要(相反的)限定语。 - Deduplicator

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接