Implementación de vistas personalizadas en C++20

Una view es un objeto que actúa como un observador sobre un rango existente, proporcionando una nueva perspectiva de los datos sin ser el dueño de ellos y sin realizar copias de los elementos. A diferencia de un contenedor, una vista es ligera y sus operaciones de copia, movimiento y asignación deben tener una complejidad $O(1)$. Esto se logra mediante la evaluación perezosa (lazy evaluation): la lógica de la transformación o el filtrado no se aplica al crear la vista, sino en el momento en que se incrementa el iterador.

Para que una clase sea considerada una std::ranges::view, debe satisfacer el concepto view, lo que implica que debe ser un range y que su construcción debe ser extremadamente barata. Si intentas implementar una vista que copie los elementos del rango base en su constructor, estarías violando la semántica de las vistas, convirtiéndola en un contenedor disfrazado y rompiendo las garantías de rendimiento de las librerías de algoritmos.

Debes usar este patrón cuando necesites implementar transformaciones de dominio específicas (como saltos de memoria, filtrados complejos o proyecciones) que deban integrarse en los pipelines de std::ranges mediante el operador |. Si intentas usar una vista para gestionar la propiedad de los datos o si la vista sobrevive al contenedor original, entrarás en el terreno del comportamiento indefinido.

#include <iostream>
#include <vector>
#include <ranges>
#include <concepts>
#include <iterator>

// Implementación de una vista que salta N elementos (stride)
// Se usa view_interface para obtener gratuitamente empty(), size(), front(), etc.
template<std::ranges::view V>
class stride_view : public std::ranges::view_interface<stride_view<V>> {
private:
    V base_ = V();
    std::ranges::range_difference_t<V> stride_ = 1;

    // El iterador interno maneja la lógica del salto
    template<bool Const>
    class iterator {
        using Parent = std::conditional_t<Const, const stride_view, stride_view>;
        using BaseIter = std::ranges::iterator_t<std::ranges::views::all_t<Parent>>;
        
        BaseIter current_ = BaseIter();
        std::ranges::range_difference_t<Parent> stride_ = 1;

    public:
        using iterator_category = std::conditional_t<std::ranges::random_access_range<Parent>, 
                                                    std::random_access_iterator_tag, 
                                                    std::bidirectional_iterator_tag>;
        using difference_type = std::ranges::range_difference_t<Parent>;
        using value_type = std::ranges::range_value_t<Parent>;
        using pointer = std::ranges::range_reference_t<Parent>;
        using reference = std::ranges::range_reference_t<Parent>;

        iterator() = default;
        iterator(BaseIter current, std::ranges::range_difference_t<Parent> stride)
            : current_(current), stride_(stride) {}

        reference operator*() const { return *current_; }
        
        iterator& operator++() {
            // El salto se realiza moviendo el iterador base N posiciones
            std::advance(current_, stride_);
            return *this;
        }

        iterator operator++(int) {
            auto tmp = *this;
            ++(*this);
            return tmp;
        }

        bool operator==(const iterator& other) const { return current_ == other.current_; }
        bool operator!=(const iterator& other) const { return !(*this == other); }

        // Soporte para iterador de acceso aleatorio
        auto operator->(const iterator& other) const { return std::distance(current_, other.current_); }
        iterator operator+(difference_type n) const {
            auto it = *this;
            std::advance(it.current_, n * stride_);
            return it;
        }
    };

public:
    stride_view() = default;
    stride_view(V base, std::ranges::range_difference_t<V> stride)
        : base_(std::move(base)), stride_(stride) {}

    auto begin() { return iterator<false>{std::ranges::begin(base_), stride_}; }
    auto end() { return std::ranges::end(base_); }
    auto begin() const requires std::ranges::range<const V> { 
        return iterator<true>{std::ranges::begin(base_), stride_}; 
    }
    auto end() const requires std::ranges::range<const V> { 
        return std::ranges::end(base_); 
    }
};

// C++23: range_adaptor_closure permite usar la vista con el operador |
struct stride_adapter_closure : std::ranges::range_adaptor_closure<stride_adapter_closure> {
    std::ranges::range_difference_t<std::ranges::views::all_t<std::vector<int>>> stride_;
    
    explicit stride_adapter_closure(auto n) : stride_(n) {}

    template<std::ranges::viewable_range R>
    auto operator()(R&& r) const {
        return stride_view(std::views::all(std::forward<R>(r)), stride_);
    }
};

auto stride(std::ranges::range_difference_t<std::vector<int>> n) {
    return stride_adapter_closure(n);
}

int main() {
    std::vector<int> data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // Uso de la vista personalizada mediante el pipeline operator |
    auto result = data | stride(2);

    std::cout << "Elementos con salto de 2: ";
    for (int v : result) {
        std::cout << v << " "; // Output: 0 2 4 6 8 10
    }
    std::cout << "\n";

    return 0;
}

Desglose técnico

La implementación comienza con stride_view, que hereda de std::ranges::view_interface<stride_view<V>>. Esto es fundamental: mediante el patrón CRTP (Curiously Recurring Template Pattern), view_interface inyecta métodos como size() o empty() basándose en las capacidades de begin() y end(). Si el iterador es random_access_iterator_tag, la vista ganará automáticamente operator[] y front().

El iterador iterator<Const> está diseñado para ser condicional. Usamos std::conditional_t para determinar si operamos sobre la versión const o no-const de la vista base, garantizando la consistencia del modelo de tipos. La operación crítica es std::advance(current_, stride_) dentro de operator++. En un iterador de acceso aleatorio, esto se traduce en una suma de punteros ($O(1)$), manteniendo la eficiencia de la vista.

Para permitir el uso de data | stride(2), implementamos std::ranges::range_adaptor_closure [C++23]. Este adaptador encapsula el parámetro stride y sobrecarga el operador de llamada, permitiendo que el compilador entienda cómo “conectar” la vista con el rango de la izquierda.

El error frecuente

Un error crítico al crear vistas es la captura de referencias temporales (dangling views). Fíjate en este caso:

// ERROR: UB - El vector es un rvalue que muere al final de la línea
auto bad_view = std::vector{1, 2, 3, 4, 5} | stride(2); 
for (int v : bad_view) { ... } // Error de segmentación o basura

Aquí, stride_view almacena una V que, al ser un std::views::all de un rvalue, contiene un std::ranges::owning_view o, de forma más peligrosa, simplemente una referencia a un objeto que ha sido destruido. Aunque std::ranges::views::all intenta prevenir esto mediante la semántica de owning views, el programador suele cometer el error de pasar una referencia a un contenedor local que sale de ámbito.

Además, si tu iterator guardara un puntero a un miembro de la vista en lugar de una copia del iterador base o una referencia al contenedor original, podrías invalidar la propiedad de la vista si esta se mueve. Siempre utiliza std::ranges::iterator_t<V> para asegurar que respetas la semántica del contenedor original.

151

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio