Directrices básicas de C++:definición de conceptos, la segunda

Directrices básicas de C++:definición de conceptos, la segunda

Permítanme continuar con las reglas para definir conceptos en las directrices. En esta publicación, la primera de las tres reglas restantes es bastante sofisticada.

Estas son las reglas para hoy:

  • T.24:Use clases de etiquetas o características para diferenciar conceptos que difieren solo en la semántica
  • T.25:Evite las restricciones complementarias
  • T.26:Preferir definir conceptos en términos de patrones de uso en lugar de sintaxis simple

La explicación de las primeras reglas es bastante concisa. Tal vez, demasiado conciso.

T.24:Use clases de etiquetas o rasgos para diferenciar conceptos que difieren solo en la semántica

Esta es la razón de esta regla de las pautas:"Dos conceptos que requieren la misma sintaxis pero que tienen una semántica diferente conducen a la ambigüedad a menos que el programador los diferencie".

Asumamos; Definí el rasgo is_contiguous. En este caso, puedo usarlo para distinguir un iterador de acceso aleatorio RA_iter de un iterador contiguo Contiguous_iter.

template<typename I> // iterator providing random access
concept bool RA_iter = ...;

template<typename I> // iterator providing random access to contiguous data
concept bool Contiguous_iter =
 RA_iter<I> && is_contiguous<I>::value; // using is_contiguous trait

Incluso puedo envolver una clase de etiqueta como is_contiguous en un concepto y usarlo. Ahora, tengo una expresión más directa de mi idea iterador contiguo Contiguous_iter.

template<typename I> concept Contiguous = is_contiguous<I>::value;

template<typename I>
concept bool Contiguous_iter = RA_iter<I> && Contiguous<I>;

Bien, permítanme explicar primero dos términos clave:rasgos y envío de etiquetas.

Características

Los rasgos son plantillas de clase que extraen propiedades de un tipo genérico.

El siguiente programa presenta para cada una de las 14 categorías de tipos primarios de la biblioteca de rasgos de tipos un tipo que satisface el rasgo específico. Las categorías de tipos principales están completas y no se superponen. Así que cada tipo es miembro de una categoría de tipo. Si marca una categoría de tipo para su tipo, la solicitud es independiente de los calificadores const o volatile.

// traitsPrimary.cpp

#include <iostream>
#include <type_traits>

using namespace std;

template <typename T>
void getPrimaryTypeCategory(){

 cout << boolalpha << endl;

 cout << "is_void<T>::value: " << is_void<T>::value << endl;
 cout << "is_integral<T>::value: " << is_integral<T>::value << endl;
 cout << "is_floating_point<T>::value: " << is_floating_point<T>::value << endl;
 cout << "is_array<T>::value: " << is_array<T>::value << endl;
 cout << "is_pointer<T>::value: " << is_pointer<T>::value << endl;
 cout << "is_null_pointer<T>::value: " << is_null_pointer<T>::value << endl;
 cout << "is_member_object_pointer<T>::value: " << is_member_object_pointer<T>::value << endl;
 cout << "is_member_function_pointer<T>::value: " << is_member_function_pointer<T>::value << endl;
 cout << "is_enum<T>::value: " << is_enum<T>::value << endl;
 cout << "is_union<T>::value: " << is_union<T>::value << endl;
 cout << "is_class<T>::value: " << is_class<T>::value << endl;
 cout << "is_function<T>::value: " << is_function<T>::value << endl;
 cout << "is_lvalue_reference<T>::value: " << is_lvalue_reference<T>::value << endl;
 cout << "is_rvalue_reference<T>::value: " << is_rvalue_reference<T>::value << endl;

 cout << endl;

}

int main(){
 
 getPrimaryTypeCategory<void>(); // (1)
 getPrimaryTypeCategory<short>(); // (1)
 getPrimaryTypeCategory<double>();
 getPrimaryTypeCategory<int []>();
 getPrimaryTypeCategory<int*>();
 getPrimaryTypeCategory<std::nullptr_t>();
 struct A{
 int a;
 int f(double){return 2011;}
 };
 getPrimaryTypeCategory<int A::*>();
 getPrimaryTypeCategory<int (A::*)(double)>();
 enum E{
 e= 1,
 };
 getPrimaryTypeCategory<E>();
 union U{
 int u;
 };
 getPrimaryTypeCategory<U>();
 getPrimaryTypeCategory<string>();
 getPrimaryTypeCategory<int * (double)>();
 getPrimaryTypeCategory<int&>(); // (2) 
 getPrimaryTypeCategory<int&&>(); // (2)
 
}

No quiero aburrirte hasta la muerte. Por lo tanto, solo queda la salida de las líneas (1).

Y aquí está el resultado de las líneas (2).

Envío de etiquetas

El envío de etiquetas le permite elegir una función en función de las propiedades de sus tipos. La decisión tiene lugar en tiempo de compilación y se utilizan los rasgos que expliqué en el último párrafo.

Un ejemplo típico de distribución de etiquetas es el algoritmo std::advance de la biblioteca de plantillas estándar. std::advance(it, n) incrementa el iterador en n elementos. El programa te muestra la idea clave.

// advanceTagDispatch.cpp

#include <iterator>
#include <forward_list>
#include <list>
#include <vector>
#include <iostream>

template <typename InputIterator, typename Distance>
void advance_impl(InputIterator& i, Distance n, std::input_iterator_tag) {
 std::cout << "InputIterator used" << std::endl; 
 while (n--) ++i;
}

template <typename BidirectionalIterator, typename Distance>
void advance_impl(BidirectionalIterator& i, Distance n, std::bidirectional_iterator_tag) {
 std::cout << "BidirectionalIterator used" << std::endl;
 if (n >= 0) 
 while (n--) ++i;
 else 
 while (n++) --i;
}

template <typename RandomAccessIterator, typename Distance>
void advance_impl(RandomAccessIterator& i, Distance n, std::random_access_iterator_tag) {
 std::cout << "RandomAccessIterator used" << std::endl;
 i += n;
}

template <typename InputIterator, typename Distance>
void advance_(InputIterator& i, Distance n) {
 typename std::iterator_traits<InputIterator>::iterator_category category; // (1)
 advance_impl(i, n, category); // (2)
}
 
int main(){
 
 std::cout << std::endl;
 
 std::vector<int> myVec{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
 auto myVecIt = myVec.begin(); // (3)
 std::cout << "*myVecIt: " << *myVecIt << std::endl;
 advance_(myVecIt, 5);
 std::cout << "*myVecIt: " << *myVecIt << std::endl;
 
 std::cout << std::endl;
 
 std::list<int> myList{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
 auto myListIt = myList.begin(); // (4)
 std::cout << "*myListIt: " << *myListIt << std::endl;
 advance_(myListIt, 5);
 std::cout << "*myListIt: " << *myListIt << std::endl;
 
 std::cout << std::endl;
 
 std::forward_list<int> myForwardList{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
 auto myForwardListIt = myForwardList.begin(); // (5)
 std::cout << "*myForwardListIt: " << *myForwardListIt << std::endl;
 advance_(myForwardListIt, 5);
 std::cout << "*myForwardListIt: " << *myForwardListIt << std::endl;
 
 std::cout << std::endl;
 
}

La expresión std::iterator_traits::iterator_category category determina la categoría del iterador en tiempo de compilación. Según la categoría del iterador, la variable más específica de la función advance_impl(i, n, category) se usa en la línea (2). Cada contenedor devuelve un iterador de la categoría de iterador que corresponde a su estructura. Por lo tanto, la línea (3) da un iterador de acceso aleatorio, la línea (4) da un iterador bidireccional y la línea (5) da un iterador directo que también es un iterador de entrada.

Desde el punto de vista del rendimiento, esta distinción tiene mucho sentido porque un iterador de acceso aleatorio puede ser incrementa más rápido que un iterador bidireccional, y un iterador bidireccional puede incrementarse más rápido que un iterador de entrada. Desde la perspectiva de los usuarios, invoca std::advance(it, 5) y obtiene la versión más rápida que satisface su contenedor.

Esto fue bastante detallado. No tengo mucho que agregar a las dos reglas restantes.

T.25:Evitar restricciones complementarias

El ejemplo de las directrices muestra restricciones complementarias.

template<typename T> 
 requires !C<T> // bad 
void f(); 

template<typename T> 
 requires C<T> 
void f();


Evítalo. Cree una plantilla sin restricciones y una plantilla restringida en su lugar.

template<typename T> // general template
 void f();

template<typename T> // specialization by concept
 requires C<T>
void f();

Incluso puede configurar la versión sin restricciones para que se elimine, de modo que solo se usen las versiones restringidas.

template<typename T>
void f() = delete;

T.26:Prefiere definir conceptos en términos de uso patrones en lugar de sintaxis simple

El título de esta guía es bastante vago, pero el ejemplo se explica por sí mismo.

En lugar de usar los conceptos has_equal y has_not_equal para definir el concepto Igualdad

template<typename T> concept Equality = has_equal<T> && has_not_equal<T>;

utiliza el patrón de uso. Esto es más legible que la versión anterior:

template<typename T> concept Equality = requires(T a, T b) {
 bool == { a == b }
 bool == { a != b }
 // axiom { !(a == b) == (a != b) }
 // axiom { a = b; => a == b } // => means "implies"
}

En este caso, el concepto Igualdad requiere que pueda aplicar ==y !=a los argumentos y ambas operaciones devuelvan bool.

¿Qué sigue?

Aquí hay una parte de la apertura de las pautas básicas de C ++ a las interfaces de plantilla:"... la interfaz de una plantilla es un concepto crítico, un contrato entre un usuario y un implementador, y debe diseñarse cuidadosamente". Verás, la próxima publicación es crítica.