Refactorización con C ++ 11

Dado el nuevo conjunto de herramientas proporcionadas por muchos progtwigdores de c ++, apuntando a la simplificación de código, expresividad, eficiencia, revise su antiguo código y realice ajustes (algunos sin sentido, algunos exitosos) para lograr sus objectives. Mientras intentamos no perder demasiado tiempo en tales trabajos y solo hacemos cambios no intrusivos y autónomos, ¿cuáles son las mejores prácticas?

Déjame tachar lo obvio:

  • Use auto para ejecutar bucles basados ​​en iterador:

    for (std::vector::const_iterator it(lala.begin()), ite(lala.end()); it != ite; ++it); // becomes for (auto it(lala.cbegin()), ite(lala.cend()); it != ite; ++it); 
  • Use un enlace para múltiples asignaciones que solo producen filas de código de estilo C ( ¿cómo asignar múltiples valores a una estructura de una vez? )

     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); 
  • Para hacer que una clase no sea heredable simplemente declare que es “definitiva” y elimine el código que logró dicho comportamiento http://www.parashift.com/c++-faq/final-classes.html

  • Utilice la palabra clave delete para ocultar explícitamente los constructores / destructores en lugar de declararlos como privados (por ejemplo, código para crear objetos basados ​​en heap, objetos no copiables, etc.)

  • Convierta los funtores triviales creados solo para facilitar la ejecución de un único algoritmo STL en funciones lambda (además de reducir el desorden del código, tendrá llamadas en línea garantizadas)

  • Simplifique el ajuste RAII de un objeto simplemente usando un puntero inteligente

  • Deshazte de bind1st, bind2nd y solo usa bind

  • Reemplace el código escrito a mano por los rasgos de tipo (Is_ptr_but_dont_call_for_const_ptrs y such :)) con el código estándar proporcionado por

  • Deje de incluir encabezados de impulso para functionallity ahora implementado en STL (BOOST_STATIC_ASSERT vs static_assert)

  • Proporcione semántica de movimiento a las clases (aunque esto no calificaría como un cambio sucio / rápido / fácil)

  • Use nullptr donde sea posible en lugar de la macro NULL y elimine el código que llenó los contenedores de punteros con 0 caducados para el tipo de objeto

     std::vector f(23); for (std::size_t i(0); i < 23; ++i) { f[i] = static_cast(0); } // becomes std::vector f(23, nullptr); 
  • Borre la syntax de acceso de datos vectoriales

     std::vector vec; &vec[0]; // access data as a C-style array vec.data(); // new way of saying the above 
  • Reemplazar throw () con noexcept (además de evitar la especificación de excepciones en desuso, obtienes algunos beneficios de velocidad 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 
  • Reemplace el código donde insertaría un tempory en un contenedor y esperaba que el optimizador eliminara la copia, con una función “emplace” donde esté disponible, para reenviar perfectamente el argumento y construir directamente un objeto en un contenedor sin temporal en todas.

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

ACTUALIZAR

La respuesta de Shafik Yaghmour fue justamente galardonada con la recompensa por tener la mayor aceptación de la audiencia.

La respuesta de R Sahu fue la aceptada, porque la combinación de características que propone captura el espíritu de la refactorización : hacer que el código sea más claro y más limpio y simple y elegante.

Agregaría constructores delegates e inicializadores de miembros en clase a la lista.

Simplificación mediante delegación de constructores e inicialización en clase

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

Con 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. Sustitución del rand

Una de las grandes ganancias en C ++ 11 tiene que ser reemplazar el uso de rand() con todas las opciones disponibles en el encabezado aleatorio . Reemplazar rand() en muchos casos debe ser sencillo.

Stephan T. Lavavej probablemente hizo este punto el más fuerte con su presentación rand () Considerado Dañino . Los ejemplos muestran una distribución entera uniforme a partir de [0,10] usando rand() :

 #include  #include  #include  int main() { srand(time(0)) ; for (int n = 0; n < 10; ++n) { std::cout << (rand() / (RAND_MAX / (10 + 1) + 1)) << ", " ; } std::cout << std::endl ; } 

y usando std :: uniform_int_distrubution :

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

Junto con esto, se debe pasar de std :: random_shuffle a std :: shuffle, que proviene del esfuerzo de Deprecate rand y Friends . Esto fue cubierto recientemente en la pregunta SO ¿ Por qué los métodos std :: shuffle están en desuso en C ++ 14? .

Tenga en cuenta que no se garantiza que las distribuciones sean consistentes en todas las plataformas .

2. Usando std :: to_string en lugar de std :: ostringstream o sprintf

C ++ 11 proporciona std :: to_string que se puede usar para convertir números a std :: string , produciría el contenido como std :: sprintf equivalente. Lo más probable es que esto se use en lugar de std :: ostringstream o snprintf . Esto es más conveniente, probablemente no hay mucha diferencia de rendimiento y podemos ver que desde el número entero rápido hasta la conversión de cadenas en el artículo de C ++, probablemente haya alternativas mucho más rápidas disponibles si el rendimiento es la principal preocupación:

 #include  #include  #include  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. Uso de constexpr en lugar de la metaprogtwigción de la plantilla

Si está tratando con literales, puede haber casos en los que el uso de funciones constexpr sobre meta-progtwigción de plantillas pueda producir código que sea más claro y posiblemente se compile más rápido. El artículo ¿Quieres velocidad? Use metaprogtwigción constexpr! proporciona un ejemplo de determinación de números primos mediante la metaprogtwigción de plantillas:

 struct false_type { typedef false_type type; enum { value = 0 }; }; struct true_type { typedef true_type type; enum { value = 1 }; }; template struct if_ { typedef U type; }; template  struct if_ { typedef T type; }; template struct is_prime_impl { typedef typename if_<(c*c > N), true_type, typename if_<(N % c == 0), false_type, is_prime_impl >::type >::type type; enum { value = type::value }; }; template struct is_prime { enum { value = is_prime_impl::type::value }; }; template <> struct is_prime<0> { enum { value = 0 }; }; template <> struct is_prime<1> { enum { value = 0 }; }; 

y usando funciones 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); } 

La versión constexpr es mucho más corta, más fácil de entender y aparentemente funciona mucho mejor que la implementación de meta-progtwigción de la plantilla.

4. Usar la inicialización del miembro de la clase para proporcionar valores predeterminados

Como se ha cubierto recientemente en ¿Ha quedado obsoleta la nueva característica de inicialización de miembro de C ++ 11 en las listas de inicialización de statement? La inicialización de miembro de clase se puede usar para proporcionar valores predeterminados y puede simplificar los casos en que una clase tiene múltiples constructores.

Bjarne Stroustrup proporciona un buen ejemplo en las preguntas frecuentes de C ++ 11, dice:

Esto ahorra un poco de tipeo, pero los beneficios reales vienen en clases con múltiples constructores. A menudo, todos los constructores usan un inicializador común para un miembro:

y proporciona un ejemplo de miembros que tienen un inicializador común:

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

y dice:

El hecho de que hash_algorithm ys tengan un único valor predeterminado se pierde en el desorden del código y podría convertirse fácilmente en un problema durante el mantenimiento. En cambio, podemos factorizar la inicialización de los miembros de datos:

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

Tenga en cuenta que en C ++ 11 una clase que utiliza los inicializadores de miembros de la clase ya no es un agregado, aunque esta restricción se elimina en C ++ 14.

5. Utilice tipos enteros de ancho fijo desde cstdint en lugar de typedefs enrollados a mano

Como el estándar C ++ 11 usa C99 como referencia normativa, también obtenemos tipos enteros de ancho fijo . Por ejemplo:

 int8_t int16_t int32_t int64_t intptr_t 

Aunque varios de ellos son opcionales, para los tipos enteros de ancho exacto, se aplica lo siguiente de la sección 7.18.1.1 de C99:

Estos tipos son opcionales Sin embargo, si una implementación proporciona tipos enteros con anchuras de 8, 16, 32 o 64 bits, sin bits de relleno y (para los tipos firmados) que tienen una representación complementaria de dos, definirá los nombres typedef correspondientes.

Para cada syntax:

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

Use la syntax de inicialización uniforme para la inicialización de variables

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

para evitar problemas como el análisis más irritante de c ++ (el rest de las razones por las que la nueva forma es superior se explican en el artículo vinculado de Herb Sutter)

  1. Cambiar std::map a std::unordered_map y std::set a std::unordered_set donde el orden de los elementos del contenedor es irrelevante, mejora significativamente el rendimiento.
  2. Usar std::map::at lugar de usar la inserción de syntax de corchetes cuadrados, cuando desee evitar las inserciones involuntarias.
  3. Use plantillas de alias cuando desee typedef plantillas typedef .
  4. Uso de listas de inicialización en lugar de bucles para inicializar contenedores STL.
  5. Reemplace las matrices de C de tamaño fijo con std :: array.

Esta publicación de blog propone la Regla del cero si todas las propiedades en una clase siguen el principio RAII, lo que permite eliminar la regla del tres / cuatro / cinco en C ++ 11.

Sin embargo, Scott Meyers muestra aquí que no escribir explícitamente el destructor, los constructores de copia / movimiento y los operadores de asignación pueden inducir problemas sutiles si se cambia ligeramente el código (por ejemplo, para la depuración). Luego recomienda declarar explícitamente por defecto (función C ++ 11) estas funciones:

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

Característica: std :: movimiento

“Exprese una clara diferencia entre copiar y mover los recursos”

 std::string tmp("move"); std::vector 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. 

Optimice funciones matemáticas simples con constexpr, especialmente si se llaman dentro de bucles internos. Esto le permitiría al comstackdor calcularlos en comstackción, ahorrándole tiempo

Ejemplo

 constexpr int fibonacci(int i) { return i==0 ? 0 : (i==1 ? 1 : fibonacci(i-1) + fibonacci(i-2)); } 

Otro ejemplo es usar std::enable_if para limitar los tipos de parámetros de plantilla permitidos en una función / clase particular de plantilla. Esto haría su código más seguro (en caso de que no haya usado SFINAE para restringir los posibles argumentos de plantilla en su código anterior) cuando implícitamente asume alguna propiedad sobre los tipos de plantilla y es solo una línea adicional de código

ejemplo:

 template < typename T, std::enable_if< std::is_abstract::value == false, bool>::type = false // extra line > void f(T t) { // do something that depends on the fact that std::is_abstract::value == false } 

Actualización 1: si tiene una matriz pequeña donde se conoce el tamaño en tiempo de comstackción y desea evitar la sobrecarga de la asignación de montón en std :: vector (lo que significa que desea la matriz en la stack), solo puede elegir C ++ 03 debía usar matrices de estilo c. Cambiar eso a std::array . Es un cambio simple que le proporciona una gran cantidad de funcionalmente presente en la asignación std :: vector + stack (mucho más rápido que la asignación de heap como dije antes).

Use punteros inteligentes. Tenga en cuenta que todavía hay una buena razón para tener punteros desnudos en algunos casos, la mejor manera de comprobar si un puntero debe ser inteligente es buscar los usos de delete en él.

No debería haber ninguna razón para usar new tampoco. Reemplace cada new con make_shared o make_unique .

Desafortunadamente make_unique no lo hizo en el estándar C ++ 11 , la mejor solución IMO es implementarlo usted mismo ( ver enlace anterior ), y poner algunas macros para verificar la versión make_unique ( make_unique está disponible en C ++ 14) .

Usar make_unique y make_shared es realmente importante para que la excepción de tu código sea segura.

  1. Prefiere las enumeraciones con ámbito a las enumeraciones sin ámbito

    • En C ++ 98 las enumeraciones, no hay ámbito para enumeraciones como el siguiente fragmento de código. Los nombres de dichos enumeradores pertenecen al scope que contiene enum, es decir, nada más en ese ámbito puede tener el mismo nombre.

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

      Sin embargo, en C ++ 11, las scoped enums pueden solucionar este problema. scoped enum se declara como enum class 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 
    • Los enumeradores de las enumeraciones de scope enums son más fuertemente tipados. Sin embargo, los enumeradores de enumeraciones unscoped enums convierten implícitamente a otros tipos

       enum Color{ blue, green, yellow }; std::vector getVector(std::size_t x); Color c = blue; if (c < 10.1) { // compare Color with double !! auto vec = getVector(c); // could be fine !! } 

      Sin embargo, las scoped enums en este caso.

       enum class Color{ blue, green, yellow }; std::vector getVector(std::size_t x); Color c = Color::blue; if (c < 10.1) { // error ! auto vec = getVector(c); // error !! } 

      Solucionarlo a través de static_cast

       if (static_cast(c) < 10.1) { auto vec = getVector(static_cast(c)); } 
    • unscoped enums pueden ser declaradas hacia adelante.

       enum Color; // error!! enum class Color; // fine 
    • Las unscoped delimitadas y unscoped abarcadas admiten la especificación del tipo subyacente. El tipo subyacente predeterminado para las scoped enums es int . Unscoped enums no Unscoped enums no tienen ningún tipo subyacente predeterminado.

  2. Usar la API de simultaneidad

    • Preferir basado en tareas basado en subprocesos

      Si desea ejecutar una función doAsyncWork forma asíncrona, tiene dos opciones básicas. Uno está basado en subprocesos

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

      El otro está basado en tareas .

       auto fut = std::async(doAsyncWork); 

      Obviamente, podemos obtener el valor de retorno de doAsyncWork través de tareas basadas más fácilmente que basadas en subprocesos . Con el enfoque task-based , es fácil, porque el futuro devuelto por std::async ofrece la función get. La función get es aún más importante si doAsyncWork emite una excepción, porque get proporciona acceso a eso también.

    • Thread-based llamadas Thread-based para la gestión manual de agotamiento de hilo, sobresuscripción, equilibrio de carga y adaptación a nuevas plataformas. Pero Task-based en Task-based via std::async con la política de lanzamiento predeterminada no presenta ninguno de estos inconvenientes.

    Aquí hay varios enlaces:

    Concurrencia en C ++

    C / C ++ Abstracciones de progtwigción para paralelismo y concurrencia

El uso de la palabra clave override

Marque las funciones virtuales en las clases derivadas como anulación (si de hecho anulan, por supuesto). Esto puede proteger contra la introducción de errores en el futuro, por ejemplo, cambiando la firma de una función virtual en una clase base y olvidando cambiar la firma en todas las clases derivadas en consecuencia.