使用C ++ 11进行重构


70

鉴于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
    
  • noexcept替换throw()(除了避免不赞成使用的异常规范,您还可以获得一些速度上的好处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
    
  • 将代码替换为您希望将容器中的临时内容推入的位置,并希望优化器将它替换为副本,并在可用的地方使用放置功能,以便完美地转发参数并直接将对象构造到容器中而无需临时所有。

    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的答案是我接受的答案,因为它提出的功能组合抓住了重构精神:使代码更清晰,更简洁,更简单,更优雅。


22
不要关闭它。这真的很有用。
Karoly Horvath

2
我不认为这是“主要基于意见的”。完全没有 但是,这是这些大问题中的一种,也确实不适合Stack Overflow格式。
康拉德·鲁道夫


4
有关使用.data()代替的问题&container[0]std::string如果您要修改内部数据,它将不起作用。为什么因为.data()forstd::string与相同.c_str()并返回常量指针。同样对于MSVC2013,push_back采用T&&和相同emplace_back
布兰登

5
使用override以指示功能覆盖在基类的虚拟函数,而不是引入一个新的/在基类隐藏的功能。我也建议不要让每节课都可以做一个期末考试。应该谨慎使用它,因为它会使测试代码变得比原本更加痛苦。
sdkljhdf hda 2014年

Answers:


19

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

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

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

29

1.替换兰特

C ++ 11的一大收获是必须用random标头rand()中的所有可用选项替换对的使用。在许多情况下,更换应该很简单。rand()

斯蒂芬·拉瓦维(Stephan T.Lavavej)可能以他的演讲rand()被认为有害来表达这一观点。这些示例显示了[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和Friends的努力而产生的。SO问题最近解决了这个问题,为什么C ++ 14中不推荐使用std :: shuffle方法?

注意,不能保证分布在平台之间一致的

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

C ++ 11提供 std :: to_string,可用于将数字转换为std :: string,它将产生与等效的std :: sprintf相同的内容。最有可能用它代替std :: 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代替模板元编程

如果要处理文字,则可能会在模板元编程上使用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
};

并说:

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

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

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


1
乍一看似乎有些过分,但是看完演示文稿后,我承认有很多我不知道的问题。出色的代码无法实现这一目标。
Nikos Athanasiou 2014年

@NikosAthanasiou考虑到您的悬赏,我的答案没有提供足够的细节?如果是这样,您想查看哪些详细信息?
Shafik Yaghmour 2014年

提供赏金是为了激发更多的答案和揭示的技巧,并让更多的人评论他们使用什么以及什么无效。您的答案既彻底又有用。我认为它不需要改进,从外观上看,它仍将是最受欢迎的并赢得赏金
Nikos Athanasiou 2014年

@Nikos Athanasiou完全同意。我喜欢他的回答。通常,当发生这种情况时(您有另一个示例可以补充和/或升级答案),我只会给出诸如“为了补充Vin ...先前给出的答案,这里是...”的引用。但是请不要忘记,在C ++ 11之前,诸如Fibbonaci计算或pow或小数之类的事情都是使用模板元编程完成的,因此我们的答案没有什么不同。
维维安·米兰达

12

对于每个语法:

std::vector<int> container;

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

2
我不明白为什么人们仍然喜欢基于迭代器的for循环,带lambdas的for_each等。基于范围的for循环产生更少的代码,并且显然更易于阅读。
MuratŞeker2014年

1
@MuratŞeker:当您有两个迭代器到两个大小相等的容器时,要容易得多。
MSalters 2014年

2
好吧,当您必须在遍历容器时修改容器时,就不能使用基于范围的迭代,并且迭代器通常会提供更多控制权。除此之外,我认为没有理由不使用它们。
voodooattack 2014年

1
stackoverflow.com/questions/21517399/…中,提到了range for的两个缺点。话虽这么说,非复杂循环使用“新”方式确实是一个好习惯。
Nikos Athanasiou 2014年

BTWstd::endl导致缓冲区刷新到控制台缓冲区。通常"\n"更快。
gradbot '19

11
  1. 更改std::mapstd::unordered_mapstd::setstd::unordered_set哪里容器的元素永远顺序是无关紧要的,显著提高了性能。
  2. 如果std::map::at要避免非自愿插入,请使用而不是方括号语法插入。
  3. 要使用模板时,请使用别名typedef模板。
  4. 使用初始化列表代替for循环来初始化STL容器。
  5. 将固定大小的C数组替换为std :: array。

5
std::uordered_set->这是一个错字。1.此更改仅可提高足够大的地图和集合的性能(否则只会降低程序的效率)。同样,如果在此类容器上进行迭代,则可能会降低性能。
构造函数


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. 优先于作用域枚举而不是无作用域枚举

    • 在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 enumsintUnscoped enums没有默认的基础类型。

  2. 使用并发API

    • 首选基于任务而不是基于线程

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

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

      另一个是基于任务的

      auto fut = std::async(doAsyncWork);
      

      显然,与基于线程相比,doAsyncWork通过基于任务的返回值更容易获得。使用这种方法很容易,因为从中返回的未来提供了get函数。如果发出异常,该功能就更为重要,因为它也提供了对该异常的访问。task-basedstd::asyncgetdoAsyncWorkget

    • Thread-based要求手动管理线程耗尽,超额订购,负载平衡以及对新平台的适应。但是Task-based通过std::async默认启动策略,这些缺点都没有。

    这里有几个链接:

    C ++中的并发

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


您可能要提及范围枚举的其他好处。同样,讨论在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样式的数组。将其更改为std::array。这是一个简单的更改,为您提供了std :: vector +堆栈分配中的许多功能(比我之前说的堆分配要快得多)。


2
切勿以这种方式评估斐波那契数。
构造函数

1
不知道为什么,特别是对于N不太大的F_N(对于大N可能有更优化的公式)。但这仅是constexpr可以做的原理的证明。我已经看到有人提倡使用小整数,对数和平方根的pow的constexpr版本,但有一些警告。如果您可以在编译时执行内部循环中调用的函数,那将是一个很好的优化。
维维安·米兰达

4

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

应该没有理由使用new两者。newmake_shared或替换每个make_unique

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

使用make_uniquemake_shared真正为了使你的代码异常安全的重要。


直接分配给智能指针与使用这些生成器功能一样安全。如果智能指针构造函数引发,它将删除构造的对象。尽管如此,make_shared仍可以使用自定义的分配器+分配器进行优化,因此共享指针的簿记将添加到该对象的前面。
Deduplicator

直接分配给智能指针与使用这些生成器功能一样安全。不是将智能指针作为参数传递给另一个函数。看到这个
sbabbi 2014年

您的意思是,如果要在构造函数中将smart_pointer作为参数传递给函数,并且还要再传递至少1个其传递/构造会引发异常的参数,对吗?看起来我们的注释都需要(反)限定符。
Deduplicator

3

使用override关键字

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.