Programando con C++20 (Parte V): CPOs (customization point objects)

 Artículos de la serie:


Entendemos como punto de personalización (customization point) toda aquella operación dentro de una biblioteca que, de forma opcional u obligatoria, delegue su comportamiento en código proporcionado por el programador-usuario [1].

Así, consideremos la plantilla de función estándar std::swap, que proporciona la posibilidad de sobrescribir su comportamiento. En estándares de C++ previos a 2020, el modo de empleo tradicional de este punto de personalización en un contexto genérico consistiría en:

  1. Introducir una declaración using std::swap.
  2. Realizar una o varias llamadas sin calificar (unqualified calls) a la función swap.

Ello permitirá que el procedimiento ADL (Argument-Dependent Lookup) encuentre y use las versiones personalizadas de swap asociadas a tipos definidos por el programador o bien que, en ausencia de ellas, se emplee por defecto la función estándar [2, 3]. Así puede comprobarse con el siguiente código ilustrativo:

   namespace my_library {       struct S { int n; };       void swap(S& s1, S& s2) noexcept       {          fmt::print("swapping objects of type S\n");          auto tmp = s1;          s1 = s2;          s2 = tmp;       }    }    // ...    using std::swap;    auto s1 = my_library::S{0},         s2 = my_library::S{1};    swap(s1, s2); // llamada a my_library::swap                  // output en terminal: swapping objects of type S    fmt::print("s1.n = {} | s2.n = {}\n", s1.n, s2.n); // output: s1.n = 1 | s2.n = 0    swap(s1.n, s2.n); // llamada a std::swap entre enteros    fmt::print("s1.n = {} | s2.n = {}\n", s1.n, s2.n); // output: s1.n = 0 | s2.n = 1

Observemos que, en el caso de objetos de tipo S, una llamada calificada de la forma std::swap(s1,s2) ignoraría la implementación de la función swap proporcionada en el espacio de nombres my_library. Es éste un error por desgracia muy común en códigos genéricos en C++, lo que ha llevado a considerar la solución idiomática anterior como especialmente proclive a errores (error-prone).

Un defecto adicional de este procedimiento consiste en que, si puntos de personalización como std::begin o std::swap impusieran ligaduras sobre sus argumentos y/o tipos retornados a través de conceptos, éstas no se llevarían a efecto de invocarse versiones definidas por el programador.

Con el fin de solventar estos y otros problemas, C++20 incluye nuevos puntos de personalización --en su mayoría definidos en el espacio de nombres std::ranges-- implementados como objetos función semirregulares y construidos como constexpr, típicamente restringidos por conceptos particulares. Nos referiremos a tales objetos como CPOs (customization point objects) [4]. Entre ellos cabe distinguir a std::ranges::begin, std::ranges::end, std::ranges::swap, std::ranges::size, std::ranges::empty, std::ranges::data, std::weak_order, std::partial_order o std::strong_order. Todos ellos pueden copiarse libremente y sus copias emplearse indistintamente.

En particular, la implementación de std::ranges::swap como objeto función adopta un esquema similar al siguiente:

   namespace std::ranges {       namespace custom_swap {          template<typename T> void swap(T&, T&) = delete;          struct Swap {             template<typename T, typename U>                requires /* restricciones sobre T y U */             constexpr void operator()(T&& a, U&& b) const noexcept(/* ... */)             {                if constexpr (/* ADL encuentra versión personalizada de swap */) {                    swap(static_cast<T&&>(a), static_cast<U&&>(b));                }                else {                   auto tmp = static_cast<remove_reference_t<T>&&>(a);                   a = static_cast<remove_reference_t<T>&&>(b);                   b = static_cast<remove_reference_t<T>&&>(tmp);                }             }          };       } // namespace custom_swap       inline namespace custom {          inline constexpr auto swap = custom_swap::Swap{};       }    } // namespace std::ranges

Toda llamada calificada std::ranges::swap(a,b) enrutará a custom_swap::Swap::operator(a,b), que a su vez realizará una llamada no-calificada swap(a,b). Esta última encontrará, como es habitual, cualquier sobrecarga definida por el programador mediante ADL. Así pues, en el caso del primer código de ejemplo del artículo, podremos escribir sencillamente:

   auto s1 = my_library::S{0},         s2 = my_library::S{1};    std::ranges::swap(s1, s2); // llamada a my_library::swap    std::ranges::swap(s1.n, s2.n); // equivalente a std::swap(s1.n, s2.n)

Tal y como se detalla en [5], el código anterior podría también realizar llamadas a swap no-calificadas, de introducir previamente una declaración using std::ranges::swap:

   using std::ranges::swap;    auto s1 = my_library::S{0},         s2 = my_library::S{1};    swap(s1, s2); // llamada a my_library::swap    swap(s1.n, s2.n); // equivalente a std::swap(s1.n, s2.n)

En efecto, durante la primera fase de búsqueda, el nombre swap se resolverá como el objeto std::ranges::swap. Dado que la búsqueda habrá encontrado un objeto y no una función, no se realizará una segunda fase de búsqueda en espacios de nombres como my_library. Así, using std::ranges::swap; swap(a,b); resultará ser equivalente en todos los casos a std::ranges::swap(a,b), que como se ha discutido más arriba realiza internamente la búsqueda de sobrecargas ADL en nombre del programador. Éste podrá optar indistintamente por llamadas calificadas o no-calificadas.

Notemos por último que, sea calificada o no la llamada a un objeto CPO, las ligaduras sobre tipos que éste defina se llevarán siempre a efecto. Así, por ejemplo, el tipo de retorno de std::ranges::begin deberá modelar el concepto std::input_or_output_iterator en todos los casos.


Referencias bibliográficas
  1. Barry Revzin - Niebloids and Customization Point Objects - https://brevzin.github.io/c++/2020/12/19/cpo-niebloid/
  2. cppreference - Argument-dependent lookup - https://en.cppreference.com/w/cpp/language/adl
  3. C++ Weekly With Jason Turner - Ep 160 - Argument Dependent Lookup (ADL) - https://youtu.be/agS-h_eaLj8
  4. Arthur O'Dwyer - A C++ Acronym Glossary - https://quuxplusone.github.io/blog/2019/08/02/the-tough-guide-to-cpp-acronyms/#cpo
  5. Eric Niebler - N4381 - Suggested Design for Customization Points - http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html

No hay comentarios:

Publicar un comentario