【问题标题】:Refactoring with C++ 11使用 C++ 11 进行重构
【发布时间】:2014-02-26 04:54:24
【问题描述】:

鉴于 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 样式代码行的多个赋值 (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);
    
  • 要使类不可继承,只需将其声明为“final”并删除实现这种行为的代码http://www.parashift.com/c++-faq/final-classes.html

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

  • 将仅为简化单个 STL 算法的执行而创建的普通函子转换为 lambda 函数(除了减少代码混乱之外,您还可以保证内联调用)

    李>
  • 仅使用智能指针

  • 来简化对象的 RAII 包装
  • 摆脱 bind1st、bind2nd 并只使用 bind

  • 提供的标准代码替换类型特征的手写代码(Is_ptr_but_dont_call_for_const_ptrs 等 :))
  • 停止在 STL(BOOST_STATIC_ASSERT 与 static_assert)中包含用于功能的提升标头(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
    

更新

The answer by Shafik Yaghmour 理所当然地获得了观众最大的接受度。

The answer by R Sahu 是我接受的一个,因为它提出的组合 功能捕获了spirit of refactoring:使代码更清晰、更简洁、更简单、更优雅。

【问题讨论】:

  • 不要关闭这个。真的很有用。
  • 我不明白这是如何“主要基于意见的”。完全没有。然而,这是这些大列表类型的问题之一,也并不真正适合 Stack Overflow 格式。
  • 使用.data() 代替&amp;container[0] 有一个问题。如果您想修改内部数据,它不适用于std::string。为什么因为.data() for std::string.c_str() 相同,并返回一个常量指针。同样对于 MSVC2013,push_back 采用 T&amp;&amp;,与 emplace_back 相同。
  • 使用override 表示函数覆盖基类中的虚函数,而不是在基类中引入新的/隐藏函数。此外,我建议不要让每一堂课都成为决赛。应该谨慎使用它,因为它会使测试代码变得比实际更痛苦。

标签: c++ c++11 refactoring


【解决方案1】:

for-each 语法:

std::vector<int> container;

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

【讨论】:

  • 我不明白为什么人们仍然更喜欢基于迭代器的 for 循环、带有 lambdas 的 for_each 等。基于范围的 for 循环结果代码更少,而且显然更易于阅读。
  • @MuratŞeker:当你有 两个 迭代器到 两个 大小相同的容器时,这会容易得多。
  • 好吧,当您必须在遍历容器时修改容器时,您不能使用基于范围的迭代,并且迭代器通常提供更多控制权。除此之外,我认为没有理由不使用它们。
  • stackoverflow.com/questions/21517399/… 提到了 ranged for 的两个缺点。话虽如此,对于非复杂循环来说,使用“新”方式确实是一个好习惯。
  • BTW std::endl 导致缓冲区刷新到控制台缓冲区。通常"\n" 更快。
【解决方案2】:

1.换兰特

C++11 的一大收获是用random header 中的所有可用选项替换rand()。在许多情况下替换 rand() 应该是直截了当的。

Stephan T. Lavavej 可能在他的演讲 rand() Considered Harmful 中表达了最强烈的观点。这些示例显示了来自[0,10] 使用rand() 的均匀整数分布:

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

随之而来的应该是从std::random_shuffle 移动到std::shuffle,这源于对Deprecate rand and Friends 的努力。最近在 SO 问题Why are std::shuffle methods being deprecated in C++14? 中对此进行了介绍。

请注意,分布不保证为consistent across platforms

2.使用 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 函数而不是模板元编程可能会生成更清晰的代码,并且可能编译得更快。文章Want speed? Use constexpr meta-programming! 提供了一个使用模板元编程确定素数的示例:

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.使用类成员初始化提供默认值

正如最近在Has the new C++11 member initialization feature at declaration made initialization lists obsolete? 中介绍的那样,类成员初始化可用于提供默认值,并可简化类具有多个构造函数的情况。

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

然后说:

hash_algorithm 和 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 中,使用 in 类成员初始化器的类是 no longer an aggregate,尽管在 C++14 中删除了此限制。

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

由于 C++11 标准使用 C99 作为规范性参考,我们也得到了fixed width integer types。例如:

int8_t
int16_t 
int32_t 
int64_t 
intptr_t

虽然其中有几个是可选的,但对于精确宽度的整数类型,C99 部分 7.18.1.1 中的以下内容适用:

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

【讨论】:

  • 一开始似乎有点矫枉过正,但看完演示后,我承认有很多我不知道的问题。伟大的代码使这一点变得困难。
  • @NikosAthanasiou 考虑到你的赏金是否我的回答没有提供足够的细节?如果是这样,您想查看哪些详细信息?
  • 提供赏金是为了激发更多的答案和技术被揭示,并让更多的人评论他们使用什么以及什么有效或无效。您的回答既详尽又有用;我认为它不需要改进,从它的外观来看,它仍然是最受欢迎的并赢得赏金
  • @Nikos Athanasiou 完全同意。我喜欢他的回答。通常当这种情况发生时(你有另一个例子来补充和/或升级一个答案)我只会给出一个引用,比如“为了补充 Vin 给出的先前答案......,这里是......”。但不要忘记,在 C++11 之前,诸如 Fibbonaci 计算或 pow 或小数字之类的事情是使用模板元编程完成的,所以我们的答案并没有那么不同。
【解决方案3】:

功能: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.

【讨论】:

  • 无法保证tmp 被移出后处于“空状态”,只能保证它是“有效但未指定”的状态。 (事实上​​,带有小对象优化的高质量字符串实现可能会保持tmp 不变。)
【解决方案4】:

使用uniform initialization syntaxvariable initialization

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

避免像c++'s most vexing parse这样的问题(新方法优越的其余原因在Herb Sutter的链接文章中解释)

【讨论】:

  • 这是个好主意,除非widget 有一个采用std::initializer_list 的构造函数。
【解决方案5】:

这篇博文提出Rule of Zero,如果一个类的所有所有权都遵循 RAII 原则,允许摆脱 C++11 中的三/四/五规则。

但是,Scott Meyers 向here 表明,如果您稍微更改代码(例如,为了调试),不明确编写析构函数、复制/移动构造函数和赋值运算符可能会导致微妙的问题。然后他建议显式声明 default(C++11 特性)这些函数:

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

【讨论】:

    【解决方案6】:

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

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

    使用 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_;
    };
    

    【讨论】:

      【解决方案7】:

      使用 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 + 堆栈分配中的许多功能(比我之前所说的堆分配快得多)。

      【讨论】:

      • 永远不要以这种方式计算斐波那契数。
      • 不知道为什么,尤其是对于 N 不是很大的 F_N(对于大 N 可能有更优化的公式)。但这只是证明您可以使用 constexpr 做什么的原理。我已经看到有人提倡使用小整数、log 和 sqrt 的 constexpr 版本的 pow,但有一些警告。如果你可以在编译时在内部循环中调用一个函数,那就是很好的优化。
      【解决方案8】:

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

      也不应该使用new。将每个new 替换为make_sharedmake_unique

      不幸的是make_uniquedidn't make it in the C++11 standard,IMO 最好的解决方案是自己实现它(参见上一个链接),并放置一些宏来检查__cplusplus 版本(make_unique 是在 C++14 中可用)。

      使用make_uniquemake_shared 真的很重要,以确保您的代码异常安全。

      【讨论】:

      • 直接分配给智能指针与使用这些制造商函数一样安全。如果智能指针构造函数抛出,它会删除构造的对象。尽管如此,make_shared 仍可以使用自定义分配器+释放器进行优化,因此共享指针的簿记会预先添加到对象中。
      • 直接分配给智能指针与使用这些制造商函数一样安全。 如果您将智能指针作为参数传递给另一个函数,情况并非如此。见this
      • 您的意思是,如果您将构造时的 smart_pointer 作为参数传递给函数,并且您至少传递了 1 个参数,其传递/构造可以引发异常,对吗?看起来我们的两个 cmets 都需要(反向)限定符。
      【解决方案9】:
      1. std::map 更改为std::unordered_map 并将std::set 更改为 std::unordered_set 在容器元素的顺序无关紧要的情况下,显着提高了性能。
      2. 当您想避免非自愿插入时,使用std::map::at 而不是使用方括号语法插入。
      3. 当您想要typedef 模板时,请使用别名模板。
      4. 使用初始化列表而不是 for 循环来初始化 STL 容器。
      5. 用 std::array 替换固定大小的 C 数组。

      【讨论】:

      • std::uordered_set -> 这是一个错字。 1.这个改动只能提升足够大的map和set的性能(否则只会降低程序的效率)。在迭代此类容器的情况下,它也可能会降低性能。
      【解决方案10】:
      1. 更喜欢有范围的枚举而不是无范围的枚举

        • 在 C++98 的枚举中,没有像下面的代码 sn-p 这样的枚举范围。此类枚举器的名称属于包含枚举的范围,即该范围内的其他任何内容都不能具有相同的名称。

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

          但是,在 C++11 中,scoped enums 可以解决此问题。 scoped enum 被声明为 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
          
        • scope enums 的枚举数具有更强的类型。但是,unscoped enums 的枚举数隐式转换为其他类型

          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 可以前向声明。

          enum Color;          // error!!
          enum class Color;    // fine
          
        • scopedunscoped 枚举都支持底层类型的规范。 scoped enums 的默认基础类型是 intUnscoped enums 没有默认的底层类型。

      2. 使用并发 API

        • 更喜欢基于任务而不是基于线程

          如果你想异步运行一个函数doAsyncWork,你有两个基本的选择。一种是基于线程的

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

          另一个是基于任务的

          auto fut = std::async(doAsyncWork);
          

          显然,我们可以通过task-basedthread-based更容易地获得doAsyncWork的返回值。使用task-based 方法很容易,因为从std::async 返回的future 提供了get 函数。如果doAsyncWork 发出异常,get 函数就更重要了,因为get 也提供了对它的访问。

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

        这里有几个链接:

        Concurrency In C++

        C/C++ Programming Abstractions for Parallelism and Concurrency

      【讨论】:

      • 您可能想提及作用域枚举的其他好处。此外,讨论在 c++11 之前存在哪些替代方案对并发性也会有所帮助。
      【解决方案11】:

      使用覆盖关键字

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-04-19
        • 1970-01-01
        • 1970-01-01
        • 2013-01-15
        • 2014-11-26
        • 2012-10-17
        • 1970-01-01
        相关资源
        最近更新 更多