[CppCon 2018] Herb Sutter: hacia un C ++ más simple y más potente


En su discurso en CppCon 2018, Herb Sutter presentó al público sus logros en dos direcciones. En primer lugar, controla la vida útil de las variables (Vida útil ), que detectará clases enteras de errores en la etapa de compilación. En segundo lugar, esta es una propuesta actualizada sobre metaclases , que permitirá evitar la duplicación de código, una vez que describa el comportamiento de una categoría de clase y luego la conecte a clases específicas con una línea.


Prólogo: ¿más = más fácil?


Se escuchan acusaciones de C ++ de que el estándar está creciendo sin sentido y sin piedad. Pero incluso los conservadores más ardientes no argumentarán que construcciones tan nuevas como range-for (ciclo de colección) y auto (al menos para iteradores) simplifican el código. Puede desarrollar criterios aproximados que (al menos una, idealmente todas) nuevas extensiones de lenguaje deben satisfacer para simplificar el código en la práctica:


  1. Reduzca, simplifique el código, elimine el código duplicado (range-for, auto, lambda, Metaclasses)
  2. Haga que el código seguro sea más fácil de escribir, evite errores y casos especiales (punteros inteligentes, tiempos de vida)
  3. Reemplace completamente las características antiguas y menos funcionales (typedef → using)

Herb Sutter destaca el "C ++ moderno", un subconjunto de características que cumplen con los estándares de codificación modernos (como las Pautas principales de C ++ ), y considera el estándar completo como un "modo de compatibilidad" que todos no necesitan saber. En consecuencia, si "C ++ moderno" no crece, entonces todo está bien.


Comprobación de la vida útil de las variables (Vida útil)


El nuevo Grupo de verificación de por vida ahora está disponible como parte del Core Guidelines Checker para Clang y Visual C ++. El objetivo no es lograr rigor y precisión absolutos, como en Rust, sino realizar controles simples y rápidos dentro de las funciones individuales.


Principios básicos de verificación


Desde el punto de vista del análisis del tiempo de vida, los tipos se dividen en 3 categorías:


  • El valor es lo que puede señalar un puntero.
  • Puntero: se refiere al valor, pero no controla su vida útil. Puede estar colgando (puntero colgante). Ejemplos: T* , T& , iteradores, std::observer_ptr<T> , std::string_view , gsl::span<T>
  • Propietario: controla la vida útil del valor. Por lo general, puede eliminar su valor antes de lo programado. Ejemplos: std::unique_ptr<T> , std::shared_ptr<T> , std::vector<T> , std::string , gsl::owner<T*>

Un puntero puede estar en uno de los siguientes estados:


  • Señale un valor almacenado en la pila
  • Señale un valor contenido "dentro" por algún propietario
  • Estar vacío (nulo)
  • Colgar (inválido)

Punteros y valores


Para cada puntero p es rastreado p s e t ( p ) - el conjunto de valores a los que puede indicar. Al eliminar un valor, su aparición en todos p s e t reemplazado por i n v á l i d oá . Al acceder a un valor de puntero p tal que i n v á l i d o p s e t ( p )á emitir un error


 string_view s; // pset(s) = {null} { char a[100]; s = a; // pset(s) = {a} cout << s[0]; // OK } // pset(s) = {invalid} cout << s[0]; // ERROR: invalid ∈ pset(s) 

Mediante anotaciones, puede configurar qué operaciones se considerarán operaciones de acceso al Valor. Por defecto: * , -> , [] , begin() , end() .


Tenga en cuenta que la advertencia se emite solo en el momento del acceso al Índice no válido. Si se elimina el Valor, pero nadie accede a este Puntero, entonces todo está en orden.


Señales y propietarios


Si puntero p indica un valor contenido dentro del propietario o entonces esto p s e t ( p ) = o .


Los métodos y funciones que toman los propietarios, se dividen en:


  • Operaciones de acceso al valor del propietario. Valor predeterminado: * , -> , [] , begin() , end()
  • Acceda a las operaciones del propio propietario, punteros v.clear() , como v.clear() . Por defecto, estas son todas las demás operaciones no constantes.
  • Acceda a las operaciones del propio propietario, punteros no invalidantes, como v.empty() . Por defecto, todas estas son operaciones constantes.

Propietario de contenido antiguo anunciado i n v á l i d o tras la eliminación del Propietario o la aplicación de operaciones invalidantes.


Estas reglas son suficientes para detectar muchos errores típicos en el código C ++:


 string_view s; // pset(s) = {null} string name = "foo"; s = name; // pset(s) = {name'} cout << s[0]; // OK name = "bar"; // pset(s) = {invalid} cout << s[0]; // ERROR 

 vector<int> v = get_ints(); int* p = &v[5]; // pset(p) = {v'} v.push_back(42); // pset(p) = {invalid} cout << *p; // ERROR 

 std::string_view s = "foo"s; cout << s[0]; // ERROR // :       std::string_view s = "foo"s // pset(s) = {"foo"s '} ; // pset(s) = {invalid} 

 vector<int> v = get_ints(); for (auto i = v.begin(); i != v.end(); ++i) { // pset(i) = {v'} if (*i == 2) { v.erase(i); // pset(i) = {invalid} } // pset(i) = {v', invalid} } // ERROR: ++i for (auto i = v.begin(); i != v.end(); ) { if (*i == 2) i = v.erase(i); // OK else ++i; } 

 std::optional<std::vector<int>> get_data(); //   ,  get_data() != nullopt for (int value : *get_data()) // ERROR cout << value; // *get_data() —     for (int value : std::vector<int>(*get_data())) // OK cout << value; 

Seguimiento de la vida útil de los parámetros de función


Cuando comenzamos a tratar con funciones en C ++ que devuelven punteros, solo podemos adivinar la relación entre la vida útil de los parámetros y el valor de retorno. Si una función acepta y devuelve punteros del mismo tipo, se supone que la función "obtiene" el valor de retorno de uno de los parámetros de entrada:


 auto f(int* p, int* q) -> int*; // pset(ret) = {p', q'} auto g(std::string& s) -> char*; // pset(ret) = {s'} 

Las funciones sospechosas se detectan fácilmente y toman el resultado de la nada:


 std::reference_wrapper<int> get_data() { //    int i = 3; return {i}; // pset(ret) = {i'} } // pset(ret) = {invalid} 

Dado que es posible pasar un valor temporal a los parámetros const T& , no se tienen en cuenta, a menos que el resultado no esté en otro lugar:


 template <typename T> const T& min(const T& x, const T& y); // pset(ret) = {x', y'} //    const T&- //        auto x = 10, y = 2; auto& bad = min(x, y + 1); // pset(bad) = {x, temp} // pset(bad) = {x, invalid} cout << bad; // ERROR 

 using K = std::string; using V = std::string; const V& find_or_default(const std::map<K, V>& m, const K& key, const V& def); // pset(ret) = {m', key', def'} std::map<K, V> map; K key = "foo"; const V& s = find_or_default(map, key, "none"); // pset(s) = {map', key', temp} ⇒ pset(s) = {map', key', invalid} cout << s; // ERROR 

También se cree que si una función acepta un puntero (en lugar de una referencia), puede ser nullptr, y este puntero no puede usarse antes de compararlo con nullptr.


Conclusión del control del tiempo de vida


Repito que Lifetime no es una propuesta para el estándar C ++ todavía, sino un intento audaz de implementar comprobaciones de por vida en C ++, donde, a diferencia de Rust, por ejemplo, nunca ha habido anotaciones correspondientes. Al principio, habrá muchos falsos positivos, pero con el tiempo, la heurística mejorará.


Preguntas de la audiencia


¿Los controles grupales de por vida proporcionan una garantía matemáticamente precisa de la ausencia de punteros colgantes?


Teóricamente, sería posible (en el nuevo código) colgar un montón de anotaciones en clases y funciones, y a cambio el compilador daría tales garantías. Pero estas comprobaciones se desarrollaron siguiendo el principio 80:20, es decir, puede detectar la mayoría de los errores utilizando un pequeño número de reglas y aplicando un mínimo de anotaciones.


Metaclases


La metaclase de alguna manera complementa el código de la clase a la que se aplica, y también sirve como nombre para un grupo de clases que satisfacen ciertas condiciones. Por ejemplo, como se muestra a continuación, la metaclase de interface hará que todas las funciones sean públicas y puramente virtuales para usted.


El año pasado, Herb Sutter realizó su primer proyecto de metaclase ( ver aquí ). Desde entonces, la sintaxis propuesta actual ha cambiado.


Para empezar, la sintaxis para usar metaclases ha cambiado:


 //  interface Shape { int area() const; void scale_by(double factor); }; //  class(interface) Shape { … } 

Se ha vuelto más largo, pero ahora hay una sintaxis natural para aplicar varias metaclases a la vez: class(meta1, meta2) .


Descripción de la metaclase


Anteriormente, una metaclase era un conjunto de reglas para modificar una clase. Ahora una metaclase es una función constexpr que toma una clase antigua (declarada en el código) y crea una nueva.


A saber, la función toma un parámetro: la metainformación sobre la clase anterior (el tipo de parámetro depende de la implementación), crea elementos de clase (fragmentos) y luego los agrega al cuerpo de la nueva clase utilizando la instrucción __generate .


Los fragmentos se pueden generar utilizando las __fragment , __inject , idexpr(…) . El orador prefirió no centrarse en su propósito, ya que esta parte todavía cambiará antes de que se presente al comité de estandarización. Se garantiza que los nombres mismos se cambiarán, se agregó doble subrayado específicamente para aclarar esto. El énfasis en el informe estaba en ejemplos que van más allá.


interfaz


 template <typename T> constexpr void interface(T source) { // source    //     .     //  ~X,  X —   . __generate __fragment struct X { virtual ~X noexcept {} }; //    static_assert, compiler.require   //   constexpr-. //      . compiler.require(source.variables().empty(), "interfaces may not contain data members"); // member_functions(), ,  tuple<…>,   for... for... (auto f : source.member_functions()) { // ,   —   / compiler.require(!f.is_copy() && !f.is_move(), "interfaces may not copy or move; consider a virtual clone()"); //   public   if (!f.has_default_access()) f.make_public(); // (1) // ,       protected/private compiler.require(f.is_public(), "interface functions must be public"); //     f.make_pure_virtual(); // (2) //   f     __generate f; } } 

Puede pensar que en las líneas (1) y (2) modificamos la clase original, pero no. Tenga en cuenta que iteramos sobre las funciones de la clase original con copia, modificamos estas funciones y luego las insertamos en una nueva clase.


Aplicación Metaclase:


 class(interface) Shape { int area() const; void scale_by(double factor); }; //  : class Shape { public: virtual ~Shape noexcept {} public: virtual int area() const = 0; public: virtual void scale_by(double factor) = 0; }; 

Depuración de Mutex


Supongamos que tenemos datos seguros sin hilos protegidos por un mutex. La depuración se puede facilitar si, en un ensamblaje de depuración, en cada llamada, se verifica si el proceso actual ha bloqueado este mutex. Para hacer esto, se escribió una clase simple de TestableMutex:


 class TestableMutex { public: void lock() { m.lock(); id = std::this_thread::get_id(); } void unlock() { id = std::thread::id{}; m.unlock(); } bool is_held() { return id == std::this_thread::get_id(); } private: std::mutex m; std::atomic<std::thread::id> id; }; 

Además, en nuestra clase MyData nos gustaría que todos los campos públicos como


 vector<int> v; 

Reemplazar con + getter:


 private: vector<int> v_; public: vector<int>& v() { assert(m_.is_held()); return v_; } 

Para las funciones, también se pueden realizar transformaciones similares.


Dichas tareas se resuelven mediante macros y generación de código. Herb Sutter declaró la guerra a las macros: no son seguras, ignoran la semántica, los espacios de nombres, etc. Cómo se ve la solución en las metaclases:


 constexpr void guarded_with_mutex() { __generate __fragment class { TestableMutex m_; // lock, unlock } } template <typename T, typename U> constexpr void guarded_member(T type, U name) { auto field = …; __generate field; auto getter = …; __generate getter; } template <typename T> constexpr void guarded(T source) { guarded_with_mutex(); for... (auto o : source.member_variables()) { guarded_member(o.type(), o.name()); } } 

Cómo usarlo:


 class(guarded) MyData { vector<int> v; Widget* w; }; MyData& x = findData("foo"); xv().clear(); // assertion failed: m_.is_held() 

actor


Bueno, incluso si protegemos algún objeto con un mutex, ahora todo es seguro para subprocesos, no hay reclamos de corrección. Pero si muchos subprocesos pueden acceder a un objeto a menudo en paralelo, el mutex se sobrecargará y habrá una gran sobrecarga para tomarlo.


La solución fundamental al problema de los mutexes con errores es el concepto de actores, cuando un objeto tiene una cola de solicitud, todas las llamadas al objeto se ponen en cola y se ejecutan una tras otra en un hilo especial.


Deje que la clase Active contenga una implementación de todo esto; de hecho, un grupo de hilos / ejecutor con un hilo. Bueno, las metaclases ayudarán a eliminar el código duplicado y poner en cola todas las operaciones:


 class(active) ImageFilter { public: ImageFilter(std::function<void(Buffer*)> w) : work(std::move(w)) {} void apply(Buffer* b) { work(b); } private: std::function<void(Buffer*)> work; } //  : class ImageFilter { public: ImageFilter(std::function<void(Buffer*)> w) : work(std::move(w)) {} void apply(Buffer* b) { a.send([=] { work(b); }).join(); } private: std::function<void(Buffer*)> work; Active a; //   ,     work } 

 class(active) log { std::fstream f; public: void info(…) { f << …; } }; 

propiedad


Hay propiedades en casi todos los lenguajes de programación modernos, y quienquiera que no las haya implementado sobre la base de C ++: Qt, C ++ / CLI, todo tipo de macros feas. Sin embargo, nunca se agregarán al estándar C ++, ya que se consideran características demasiado limitadas, y siempre existía la esperanza de que alguna propuesta las implementara como un caso especial. Bueno, ¡pueden implementarse en metaclases!


 //  class X { public: class(property<int>) WidthClass { } width; }; //  class X { public: class WidthClass { int value; int get() const; void set(const int& v); void set(int&& v); public: WidthClass(); WidthClass(const int& v); WidthClass& operator=(const int& v); operator int() const; //   move! WidthClass(int&& v); WidthClass& operator=(int&& v); } width; }; 

Puede configurar su propio getter y setter:


 class Date { public: class(property<int>) MonthClass { int month; auto get() { return month; } void set(int m) { assert(m > 0 && m < 13); month = m; } } month; }; Date date; date.month = 15; // assertion failed 

Idealmente, quiero escribir la property int month { … } , pero incluso dicha implementación reemplazará el zoológico de extensiones de C ++ que inventan propiedades.


Conclusión de Metaclase


Las metaclases son una gran característica nueva para un lenguaje ya complejo. ¿Vale la pena? Estos son algunos de sus beneficios:


  • Deje que los programadores expresen sus intenciones más claramente (quiero escribir actor)
  • Reduzca la duplicación de código y simplifique el desarrollo y mantenimiento del código que sigue ciertos patrones
  • Elimine algunos grupos de errores comunes (será suficiente para encargarse de todas las sutilezas una vez)
  • ¿Permitir deshacerse de las macros? (Herb Sutter es muy beligerante)

Preguntas de la audiencia


¿Cómo depurar metaclases?


Al menos para Clang, existe una función intrínseca que, si se llama, imprimirá el contenido real de la clase en tiempo de compilación, es decir, lo que se obtiene después de aplicar todas las metaclases.


Se solía decir que podía declarar no miembros como swap y hash en metaclases. ¿A dónde fue ella?


La sintaxis se desarrollará más.


¿Por qué necesitamos metaclases si ya se han adoptado conceptos para la estandarización?


Estas son cosas diferentes. Las metaclases son necesarias para definir partes de una clase, y los conceptos verifican si una clase coincide con cierto patrón usando ejemplos de clase. De hecho, las metaclases y los conceptos funcionan bien juntos. Por ejemplo, puede definir el concepto de un iterador y la metaclase de un "iterador típico" que define algunas operaciones redundantes a través del resto.

Source: https://habr.com/ru/post/es425873/


All Articles