Directrices básicas de C++:interfaces I

Directrices básicas de C++:interfaces I

Las interfaces son un contrato entre un proveedor de servicios y un consumidor de servicios. Las Directrices básicas de C++ tienen 20 reglas para corregirlas porque "las interfaces son probablemente el aspecto más importante de la organización del código".

Antes de sumergirme en las reglas, aquí hay una descripción general de las 20 reglas.

  • I.1:Hacer interfaces explícitas
  • I.2:Evitar variables globales
  • I.3:Evitar singletons
  • I.4:Cree interfaces precisas y fuertemente tipeadas
  • I.5:Indique las condiciones previas (si las hay)
  • I.6:Preferir Expects() para expresar condiciones previas
  • I.7:Condiciones posteriores del estado
  • I.8:Preferir Ensures() para expresar condiciones posteriores
  • I.9:Si una interfaz es una plantilla, documente sus parámetros usando conceptos
  • I.10:Usar excepciones para señalar una falla al realizar una tarea requerida
  • I.11:Nunca transfiera la propiedad mediante un puntero sin formato (T* )
  • I.12:Declarar un puntero que no debe ser nulo como not_null
  • I.13:No pase una matriz como un solo puntero
  • I.22:Evite la inicialización compleja de objetos globales
  • I.23:Mantenga bajo el número de argumentos de función
  • I.24:Evitar parámetros adyacentes no relacionados del mismo tipo
  • I.25:Preferir clases abstractas como interfaces para jerarquías de clases
  • I.26:si desea una ABI de compilador cruzado, use un subconjunto de estilo C
  • I.27:Para una ABI de biblioteca estable, considere el modismo de Pimpl
  • I.30:Encapsular infracciones de reglas

Haré que mi discusión de las reglas no sea tan elaborada porque hay demasiadas reglas. Mi idea es que en este post escriba sobre las diez primeras reglas y en el próximo post sobre las 10 restantes. Entonces, comencemos.

I.1:Hacer explícitas las interfaces

Esta regla trata sobre la corrección y los medios:las suposiciones deben establecerse en una interfaz. De lo contrario, se pasan por alto fácilmente y son difíciles de probar.

int round(double d)
{
 return (round_up) ? ceil(d) : d; // don't: "invisible" dependency
}

Por ejemplo, la función round no expresa que su resultado dependa de la dependencia invisible round_up.

I.2:Evitar variables globales

Esta regla es bastante obvia, pero el énfasis recae en las variables globales mutables. Las constantes globales están bien porque no pueden introducir una dependencia en la función y no pueden estar sujetas a condiciones de carrera.

I.3:Evitar singletons

Los singletons son objetos globales bajo el capó, por lo tanto, debe evitarlos.

I.4:Hacer interfaces precisas y fuertemente tipadas

El motivo de esta regla lo deja claro:"Los tipos son la mejor y más simple documentación, tienen un significado bien definido y se garantiza que se verificarán en el momento de la compilación".

Echa un vistazo a un ejemplo:

void draw_rect(int, int, int, int); // great opportunities for mistakes
draw_rect(p.x, p.y, 10, 20); // what does 10, 20 mean?

void draw_rectangle(Point top_left, Point bottom_right);
void draw_rectangle(Point top_left, Size height_width);

draw_rectangle(p, Point{10, 20}); // two corners
draw_rectangle(p, Size{10, 20}); // one corner and a (height, width) pair

¿Qué tan fácil es usar la función draw_rect de manera incorrecta? Compare esto con la función dibujar_rectángulo. El compilador garantiza que el argumento es un objeto Point o Size.

Por lo tanto, debe buscar en su proceso de mejora de código funciones con muchos argumentos de tipo incorporados y, lo que es peor, funciones que acepten void* como parámetro.


I.5:Indicar condiciones previas (si las hay)

Si es posible, las condiciones previas tales que x en doble raíz cuadrada (doble x) no debe ser negativa, deben expresarse como afirmaciones.

Expects() de la biblioteca de soporte de Directrices (GSL) le permite expresar su condición previa directamente.

double sqrt(double x) { Expects(x >= 0); /* ... */ }

Los contratos, que constan de condiciones previas, condiciones posteriores y aserciones, pueden formar parte del próximo estándar C++20. Ver la propuesta p03801.pdf.

I.6:Preferir Expects() para expresar condiciones previas

Eso es similar a la regla anterior, pero el énfasis está en un aspecto diferente. Debe usar Expects() para expresar condiciones previas y no, por ejemplo, una expresión if, un comentario o una declaración de afirmación().

int area(int height, int width)
{
 Expects(height > 0 && width > 0); // good
 if (height <= 0 || width <= 0) my_error(); // obscure
 // ...
}

La expresión Expects() es más fácil de detectar y quizás se pueda verificar con el próximo estándar C++20.

I.7:Indicar poscondiciones, I.8:Preferir asegura () para expresar condiciones posteriores

De acuerdo con los argumentos de una función, hay que pensar en sus resultados. Por lo tanto, las reglas de condición posterior son bastante similares a las reglas de condición previa anteriores.

I.9:Si una interfaz es un plantilla, documente sus parámetros usando conceptos

Lo conseguiremos con alta probabilidad con conceptos de C++20. Los conceptos son predicados en parámetros de plantilla que se pueden evaluar en tiempo de compilación. Un concepto puede limitar el conjunto de argumentos que se aceptan como parámetros de plantilla. Ya escribí cuatro publicaciones sobre conceptos, porque hay mucho más en los conceptos.

La regla de las Directrices básicas de C++ es bastante sencilla. Deberías aplicarlos.

template<typename Iter, typename Val>
requires InputIterator<Iter> && EqualityComparable<ValueType<Iter>>, Val>
Iter find(Iter first, Iter last, Val v)
{
 // ...
}

El algoritmo de búsqueda genérico requiere que el parámetro de plantilla Iter sea un InputIterator y el valor subyacente del parámetro de plantilla Iter sea EqualityComparable. Si invoca el algoritmo de búsqueda con un argumento de plantilla que no cumple con este requisito, obtendrá un legible y fácil de entender el mensaje de error.


I. 10:Use excepciones para señalar una falla al realizar una tarea requerida

Esta es la razón:"No debería ser posible ignorar un error porque eso podría dejar el sistema o un cálculo en un estado indefinido (o inesperado)".

La regla proporciona un mal y un buen ejemplo.

int printf(const char* ...); // bad: return negative number if output fails

template <class F, class ...Args>
// good: throw system_error if unable to start the new thread
explicit thread(F&& f, Args&&... args);

En el peor de los casos, puede ignorar la excepción y su programa tiene un comportamiento indefinido.

Si no puede usar excepciones, debe devolver un par de valores. Gracias al enlace estructurado de características de C++17, puede hacerlo con bastante elegancia.

auto [val, error_code] = do_something();
if (error_code == 0) {
 // ... handle the error or exit ...
}
// ... use val ...

¿Qué sigue?

Eso es bastante fácil de adivinar. En la siguiente publicación, escribo sobre las reglas restantes para punteros, inicialización de objetos globales, parámetros de función, clases abstractas y ABI (interfaz binaria de aplicación). Hay mucho que saber sobre un buen diseño de interfaz.