用C ++重构11
鉴于由c ++提供的大量程序员所提供的新工具集,针对代码简化,expression性,效率,浏览旧代码并进行调整(一些毫无意义,一些成功)来实现其目标。 尽pipe不要在这样的劳动中浪费太多时间,而只是做出非侵入性和自我包含的变化,那么最佳实践是什么?
让我把这个显而易见的事
-
使用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);
-
为了使一个类不可inheritance,只需声明它为“final”并删除实现了这种行为的代码。http://www.parashift.com/c++-faq/final-classes.html
-
使用delete关键字可以显式隐藏构造函数/析构函数,而不是将其声明为私有的(例如创build基于堆的对象的代码,不可复制的对象等)
-
为了简化单个STLalgorithm的执行,创build一些简单的函子(除了减less代码混乱之外,还可以保证内联调用)
-
通过使用智能指针来简化对象的RAII包装
-
摆脱bind1st,bind2nd,只使用绑定
-
将types特征的手写代码(Is_ptr_but_dont_call_for_const_ptrs <>等:)replace为由<type_traits>提供的标准代码
-
停止包含函数的boost头文件现在在STL中实现(BOOST_STATIC_ASSERT vs static_assert)
-
提供移动语义到类(虽然这不会被认为是一个脏/快速/简单的变化)
-
在可能的地方使用nullptr而不是NULLmacros,并删除用0填充的指针容器的代码转换为对象types
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);
-
清除vector数据访问语法
std::vector<int> vec; &vec[0]; // access data as a C-style array vec.data(); // new way of saying the above
-
用noexceptreplacethrow()(除了避免过时的exception说明,你会得到一些速度的好处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为容器中的临时文件,并希望优化器将文件拷贝掉,在可用的情况下使用“emplace”函数,以便将参数完美地转发并直接将对象构造到容器中所有。
vecOfPoints.push_back(Point(x,y,z)); // so '03 vecOfPoints.emplace_back(x, y, z); // no copy or move operations performed
UPDATE
Shafik Yaghmour的回答正确地被授予观众最大的认同。
R Sahu的回答是我所接受的,因为它所提出的特征组合体现了重构的精神 :使代码更清晰,更简单,更优雅。
我会添加委托构造函数和类内成员初始化到列表中。
使用委托构造函数和类内初始化简化
用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_; };
1.更换兰德
C ++ 11的一大优势就是将rand()
replace为随机头中可用的所有选项。 在很多情况下replacerand()
应该是直接的。
Stephan T. Lavavej可能会把他的陈述rand()认为是有害的 。 这些例子使用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 ,这是努力抛弃rand和 friends 。 这是最近在SO问题中被覆盖了为什么std :: shuffle方法在C ++ 14中被弃用? 。
请注意,这些分发不能保证在各个平台上保持一致 。
2.使用std :: to_string而不是std :: ostringstream或sprintf
C ++ 11提供了std :: to_string ,它可以用来将数字转换为std :: string ,它将产生相当于std :: sprintf的内容 。 这很可能会被用来代替std :: ostringstream或者snprintf
。 这更方便,可能没有太大的性能差异,我们可以从快速整数到C ++文章中的string转换中看到,如果性能是主要关心的话,可能有更快的select可用:
#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版本比模板元编程实现要短得多,更容易理解,并且performance得更好。
4.使用类成员初始化来提供默认值
正如最近在新的C ++ 11成员初始化特性中声明的初始化列表已经被覆盖了吗? 类成员初始化可以用来提供默认值,并可以简化一个类有多个构造函数的情况。
Bjarne Stroustrup在C ++ 11 FAQ中提供了一个很好的例子,他说:
这节省了一些input,但真正的好处来自多个构造函数的类。 通常,所有构造函数都为一个成员使用一个通用的初始化方法:
并提供了一个具有共同初始值的成员的例子:
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中,在类成员初始化器中使用的类不再是聚合,尽pipe在C ++ 14中删除了这个限制。
5.使用cstdint中的固定宽度整数types而不是手动滚动typedef
由于C ++ 11标准使用C99作为标准参考,所以我们也得到了固定宽度的整数types 。 例如:
int8_t int16_t int32_t int64_t intptr_t
虽然其中有几个是可选的,但是对于确切的宽度整数types,C99第7.18.1.1
节中的以下7.18.1.1
适用:
这些types是可选的。 但是,如果一个实现提供宽度为8,16,32或64位的整数types,没有填充位,并且(对于有符号types)有二进制补码表示,则它应该定义相应的typedef名称。
For-each语法:
std::vector<int> container; for (auto const & i : container) std::cout << i << std::endl;
使用统一的初始化语法进行variables初始化
widget w(x); // old widget w{x}; // new
为了避免像c ++最令人头疼的parsing (新的方式优越的原因的其他原因解释在Herb Sutter的链接文章)
这个博客文章提出了“零规则”,如果所有的所有者都遵循RAII原则,允许在C ++ 11中摆脱三/四/五规则。
然而,Scott Meyers 在这里表明,如果你稍微改变你的代码(比如debugging),不要显式地写析构函数,复制/移动构造函数和赋值操作符会引发细微的问题。 然后他build议显式声明这些函数的默认 (C ++ 11function):
~MyClass() = default; MyClass( const MyClass& ) = default; MyClass( MyClass&& ) = default; MyClass& operator=( const MyClass& ) = default; MyClass& operator=( MyClass&& ) = default;
特征: 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.
- 将
std::map
更改为std::unordered_map
,将std::set
更改为std::unordered_set
容器元素的顺序无关紧要,从而显着提高了性能。 - 当你想避免不自主的插入时,使用
std::map::at
而不是使用方括号语法插入。 - 当你想要
typedef
模板时使用别名模板。 - 使用初始化列表而不是for循环来初始化STL容器。
- 用std :: arrayreplace固定大小的C数组。
使用constexpr优化简单的math函数,尤其是在内部循环内部调用时。 这将允许编译器在编译时计算它们,节省您的时间
例
constexpr int fibonacci(int i) { return i==0 ? 0 : (i==1 ? 1 : fibonacci(i-1) + fibonacci(i-2)); }
另一个例子是使用std::enable_if
在特定的模板函数/类中限制允许的模板参数types。 这将使你的代码更安全(如果你没有使用SFINAE来限制旧代码中的可能的模板参数),当你隐式地假设一些关于模板types的属性,它只是一个额外的代码行
例:
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(意味着:你想要堆栈中的数组)的开销,那么你只能selectC ++ 03是使用c风格的数组。 将其更改为std::array
。 这是一个简单的改变,它提供了很多std :: vector +堆栈分配function(比我之前说的快得多)。
使用智能指针。 请注意,在某些情况下,仍然有充分的理由使用裸指针,检查指针是否智能的最好方法是查找delete
的用法。
应该没有理由使用new
。 用make_shared
或make_unique
replace每个new
。
不幸的是, make_unique
并没有在C ++ 11标准中实现 ,最好的解决scheme是自己实现它( 见前面的链接 ),并放入一些macros来检查__cplusplus
版本( make_unique
在C ++ 14中可用) 。
使用make_unique
和make_shared
是非常重要的,以使您的代码exception安全。
-
将范围枚举优先于非范围枚举
-
在C ++ 98的枚举中,没有像下面的代码片段那样的枚举范围。 这样的枚举器的名字属于包含枚举的范围,即在该范围内没有其他的可以具有相同的名称。
enum Color{ blue, green, yellow }; bool blue = false; // error: 'blue' redefinition
但是,在C ++ 11中,
scoped enums
可以解决这个问题。scoped enum
被声明为varenum 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
器的types更强。 但是,非unscoped enums
隐式转换为其他typesenum 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
-
scoped
和非unscoped
枚举都支持基础types的规范。scoped enums
的默认基础types是int
。Unscoped enums
没有默认的基础types。
-
-
使用并发API
-
首选基于线程的任务
如果你想asynchronous运行一个函数
doAsyncWork
,你有两个基本的select。 一个是基于线程的int doAsyncWork(); std::thread t(doAsyncWork);
另一个是基于任务的 。
auto fut = std::async(doAsyncWork);
显然,我们可以比基于线程的更容易地通过基于任务的获取
doAsyncWork
的返回值。 使用task-based
方法很容易,因为从std::async
返回的将来会提供get函数。 如果doAsyncWork
发出exception,则get
函数更为重要,因为get
提供doAsyncWork
的访问。 -
Thread-based
调用可以手动pipe理线程耗尽,超额预订,负载平衡以及适应新平台。 但是Task-based
std::async
与默认的启动策略没有任何的缺点。
这里有几个链接:
并发在C ++中
用于并行和并发的C / C ++编程抽象
-
使用覆盖关键字
将派生类中的虚拟函数标记为覆盖(如果它们确实被覆盖)。 这可以防止未来引入错误,例如,通过更改基类中的虚函数的签名,并忘记相应地更改所有派生类中的签名。