用C 11进行重构

Refactoring with C++ 11

本文关键字:重构      更新时间:2023-10-16

给定了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);
    
  • 要使类不继承的类将其声明为"最终",然后删除实现此类行为的代码>

  • 使用删除关键字明确隐藏构造函数/驱动器,而不是私有声明(例如,代码创建基于堆的对象,不可复制的对象等)

  • 创建的转弯函数只是为了将单个STL算法的执行面对 lambda functions(除了减少代码混乱外,您还可以保证嵌入式电话)

  • 仅使用智能指针

  • 简化对象的RAII包装
  • 摆脱bind1st,bind2nd和只需使用 bind

  • 替换类型特征的手写代码(is_ptr_but_dont_call_for_for_const_ptrs&lt;> y> :))用&lt提供的标准代码;type_traits>

  • 停止包括功能功能的增强标头,现在将功能固定在stl中(boostrongtatic_assert vs 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()有效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
    
  • 替换代码在容器中推动tempory的位置,并希望优化器能够将复制物具倒副本,并用" 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的答案是我接受的,因为它提出的功能的组合捕获了重构的精神:使代码更清晰,更清洁,更简单,更优雅。

1。更换rand

C 11中的最大收益之一必须用随机标头中的所有选项代替rand()的使用。在许多情况下,替换rand()应该直截了当。

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

随之而来的是,应该从std :: random_shuffle到std :: Shuffle,这是由于贬低兰德和朋友的努力而出现的。最近在SO问题中介绍了这一点,为什么STD :: Shuffle方法在C 14中被弃用?

请注意,不能保证在平台之间保持一致。

2。使用std :: to_string代替std :: ostringstream或sprintf

c 11提供std :: to_string,可用于将数字转换为std ::字符串,它将作为等效的std :: sprintf产生内容。最有可能将其代替STD :: OSTRINGSTREAM或snprintf。这更方便,可能没有太大的性能差异,我们可以从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常见问题中提供了一个很好的例子,他说:

这节省了一些打字,但是真正的好处是与多个构造函数的课程。通常,所有构造函数都使用一个共同的初始化器作为成员:

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

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中,使用类成员初始化器中的类中的类别不再是骨料,尽管在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名称。

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

通过使用委派构造函数和课堂初始化

来简化

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

for-east语法:

std::vector<int> container;
for (auto const & i : container)
  std::cout << i << std::endl;
  1. std::map更改为std::unordered_map,将std::set更改为 std::unordered_set在容器的元素元素的情况下无关紧要,可以显着提高性能。
  2. 当您想避免非自愿插入时,使用std::map::at而不是使用Square Bracket语法插入。
  3. 当您要typedef模板时使用别名模板。
  4. 使用初始化列表而不是用于初始化STL容器的循环。
  5. 用std :: Array替换固定尺寸的C数组。

使用统一初始化语法对于可变初始化

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

避免C 最烦人的解析(Herb Sutter的链接文章中解释了新方法的其余原因)

如果所有所有权均遵循RAII原则,则此博客文章提出了零规则,从而可以摆脱C 11中的三/四/五规则。<<<<<<<<<</p>

但是,斯科特·迈耶斯(Scott Meyers)在这里表明,如果您稍微更改代码(例如,用于调试),则不明确编写灾难,复制/移动构造函数和作业操作员可能会引起微妙的问题。然后,他建议明确声明默认(C 11功能)这些功能:

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

功能: std ::移动

"在复制和移动资源之间明确差异"

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.
  1. 更喜欢范围的枚举而不是未指示的枚举

    • 在C 98中,枚举没有像以下代码段这样的枚举范围。这种枚举者的名称属于包含枚举的范围,即该范围中没有其他名称。

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

      显然,我们可以比基于基于 thread的更轻松地获得doAsyncWork的返回值。使用task-based方法,这很容易,因为从std::async返回的未来提供了GET功能。如果doAsyncWork发出异常,则get功能更为重要,因为get也提供了对此的访问。

    • Thread-based要求手动管理线程耗尽,超额订阅,负载平衡和适应新平台。但是 Task-based通过 std::async,默认启动策略均未遭受这些缺陷。

    这里有几个链接:

    C 中的并发

    c/c 平行性和并发的摘要

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

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

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

不幸的是make_unique没有在C 11标准中取得成功,最好的解决方案是自己实现它(请参阅上一个链接),并放置一些宏来检查__cplusplus版本(make_unique在C 14中可用)。

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

使用>Override 关键字

标记派生类中的虚拟函数为覆盖(如果确实确实覆盖了)。这可以防止将来引入错误,例如通过更改基类中虚拟函数的签名,并忘记在所有派生类中更改签名。