¿Pueden las sobrecargas de funciones genéricas estar abiertas para otras sobrecargas?


Quiero implementar algunos algoritmos genéricos y tengo una serie de ideas sobre cómo se podrían implementar algoritmos especializados dependiendo de ciertos rasgos de las entidades con las que se usa el algoritmo. Sin embargo, parece probable que no se me ocurrieron todos los rasgos especiales y me gustaría implementar la versión genérica para que pudieran funcionar con otra versión especializada.

Por ejemplo, considere distance(begin, end) (sí, sé que está en la biblioteca standad; sin embargo, es agradable y simple y puede ser utilizado para demostrar mi problema). Una versión general podría verse así (estoy usando std::ptrdiff_t en lugar de std::iterator_traits<It>::difference_type como otra simplificación):

template <typename It>
auto distance(It it, It end) -> std::ptrdiff_t {
    std::ptrdiff_t size{};
    while (it != end) {
        ++it;
        ++size;
    }
    return size;
}

Por supuesto, si el tipo iterador es un iterador de acceso aleatorio, es mucho mejor implementar el algoritmo utilizando la diferencia entre los dos iteradores. Ingenuamente solo añadiendo

template <typename It>
auto distance(It begin, It end)
     -> typename std::enable_if<is_random_access_v<It>, std::ptrdiff_t>::type {
    return end - begin;
}

No funciona del todo: ambas implementaciones son igual de buenas coincidencias para iteradores de acceso aleatorio, es decir, el compilador las considera ambiguas. Easy el enfoque para hacer frente a la situación es cambiar la implementación general para que sea aplicable solo para iteradores de acceso no aleatorio. Es decir, las elecciones SFINAE se hacen de tal manera que son mutuamente excluyentes mientras que también cubren todo el espacio.

Desafortunadamente, el conjunto de implementaciones sigue cerrado: sin cambiar la firma de, al menos, una de las implementaciones no puedo agregar otra implementación en caso de que tenga otra idea para una implementación genérica aprovechando propiedades especiales. Por ejemplo, si quiero agregar procesamiento especial para rangos segmentados (idea: cuando la secuencia subyacente consiste en segmentos tal cual, por ejemplo, el caso de std::deque<...> o std::istreambuf_iterator<cT>, procesar segmentos individualmente) sería necesario cambiar la implementación general para que sea aplicable solo cuando las secuencias no sean un acceso aleatorio y no sean una secuencia segmentada. Por supuesto, si yo controlo la implementación, eso se puede hacer. Un usuario no sería capaz de extender el conjunto de implementaciones genéricas.

Soy consciente de que las funciones se pueden sobrecargar para tipos de iteradores especiales. Sin embargo, eso requeriría que cada vez que se agregue un iterador con capacidades especiales, tendría que implementar las funciones respectivas. El objetivo es permitir la adición de implementaciones genéricas que son mejoras en caso de que las entidades con las que se utilizan expongan facilidades adicionales. Es similar a las diferentes categorías de iteradores, aunque las propiedades son ortogonales a la categorías de iteradores.

Por lo tanto, mi pregunta es:

  • ¿Se pueden implementar algoritmos genéricos de tal manera que se pueda agregar una nueva idea de mejora sin cambiar las implementaciones existentes y, si es así, cómo?
  • Seguimiento opcional (estoy principalmente interesado en la pregunta anterior, pero esta también podría ser interesante): Si eso no es posible, ¿se agregaría esta habilidad con conceptos?
Author: Dietmar Kühl, 2014-12-16

5 answers

Un enfoque es un mecanismo de sobrecarga basado en la clasificación. Asigne a cada sobrecarga un rango y deje que la resolución de sobrecarga haga el resto.
Estos son los rasgos ayudantes:

template <unsigned i> struct rank : rank<i - 1> {};

template <> struct rank<0> {};

using call_ranked = rank<256>;

Y este es un ejemplo de uso:

template <typename It>
auto distance_ranked(rank<0>, It it, It end) -> std::size_t {
    std::size_t size{};
    while (it != end) {
        ++it;
        ++size;
    }
    return size;
}

template <typename It>
auto distance_ranked(rank<1>, It begin, It end)
     -> typename std::enable_if<is_random_access_v<It>, std::size_t>::type {
    return end - begin;
}

// Delegating function template:
template <typename... Args>
auto distance(Args&&... args)
    -> decltype(distance_ranked(call_ranked(), std::forward<Args>(args)...)) {
    return      distance_ranked(call_ranked(), std::forward<Args>(args)...);
}

Demo.
Un rango con un número más alto tiene más prioridad que uno con un número más bajo. Es decir, rank<1> hace que la segunda sobrecarga se seleccione sobre la primera (rank<0>) si las coincidencias serían idénticas.

Si desea agregar un implementación basada en segmentos, use eso como condición para enable_if. Presumiblemente, los rangos segmentados y los rangos de acceso aleatorio serían mutuamente excluyentes, pero si no lo son, asigne al acceso aleatorio una prioridad más alta. La directriz general podría ser: Cuanto más eficiente es una implementación, mayor es su rango.
Usando este método, otras implementaciones no deberían verse afectadas al introducir una nueva. Sin embargo, uno tiene que asegurarse de que dos categorías cualesquiera con intersecciones no vacías (que no están cubiertos por una categoría con un rango más alto) tienen un rango diferente, lo que constituye una desventaja notable.

 18
Author: Columbo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-16 18:14:07

Concepts prefiere una sobrecarga más restringida sobre una sobrecarga menos restringida, por lo que no necesita excluir el dominio de una implementación restringida del dominio de una implementación sin restricciones como lo haría con SFINAE. Su implementación básica podría escribirse como:

template <typename It>
std::size_t distance(It it, It end) {
    std::size_t size{};
    while (it != end) {
        ++it;
        ++size;
    }
    return size;
}

template <typename It>
requires is_random_access_v<It>
std::size_t distance(It begin, It end) {
    return end - begin;
}

No es necesario excluir iteradores de acceso aleatorio (el dominio de la sobrecarga restringida) del dominio de la sobrecarga no restringida.

Si todos los iteradores segmentados son al azar o todos los iteradores aleatorios están segmentados, luego de nuevo los conceptos preferirán la sobrecarga más restringida y todo está bien. Solo tienes que añadir la nueva sobrecarga restringida:

template <typename It>
requires SegmentedIterator<It>
std::size_t distance(It begin, It end) {
    // ...
}

Si tiene sobrecargas restringidas con rangos superpuestos, pero ninguna subsume las restricciones de la otra, la resolución de sobrecarga es ambigua al igual que con SFINAE. Sin embargo, romper la ambigüedad es un poco más simple, ya que solo es necesario agregar una nueva sobrecarga para especificar el comportamiento en la región de solapamiento:

template <typename It>
requires SegmentedIterator<It> && is_random_access_v<It>
std::size_t distance(It begin, It end) {
    // ...
}

SFINAE requeriría excluir adicionalmente la superposición del dominio de las otras sobrecargas, pero los Conceptos preferirán esta sobrecarga más restringida sin requerir cambios en las sobrecargas para SegmentedIterator y is_random_access_v.

Concepts permite al usuario extender fácilmente su implementación genérica con sobrecargas ortogonales. Las sobrecargas no ortogonales requieren más esfuerzo para especificar el comportamiento en la "superposición", pero no requieren cambios en el código original como SFINAE gustar.

 9
Author: Casey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-16 18:51:47

Tenga en cuenta que puede "emular" conceptos en C++11 usando el truco de Walter Brown void_t (ver void_t "can implement concepts"?).

Entonces puede proporcionar una implementación base como una plantilla de clase

template <typename It, class=void>
struct dist_impl {
auto operator()(It it, It end) -> std::size_t {
    std::size_t size{};
    while (it != end) {
        ++it;
        ++size;
    }
    cout << "base distance\n";
    return size;
}
};

Y hacer especialización parcial con void_t para que el compilador elija la coincidencia más especializada

template <typename It>
struct dist_impl<It, void_t<typename std::enable_if<is_random_access<It>::value>::type>> {
auto operator()(It begin, It end) -> std::size_t {
    cout << "random distance\n";
    return end - begin;
}
};

Se aplican las mismas consideraciones de "ortogonalidad".

Aquí hay un ejemplo completo: http://coliru.stacked-crooked.com/a/e4fd8d6860119d42

 3
Author: Roman L,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:52:33

Sobrecarga

Primero, echemos un vistazo al manejo de la función distance. Usando la biblioteca Tick, puede implementar rasgos de concepto para iteradores traversals en C++ de la siguiente manera:

TICK_TRAIT(is_incrementable)
{
    template<class T>
    auto requires_(T&& x) -> tick::valid<
        decltype(x++),
        decltype(++x)
    >;
};

TICK_TRAIT(is_decrementable, is_incrementable<_>)
{
    template<class T>
    auto requires_(T&& x) -> tick::valid<
        decltype(x--),
        decltype(--x)
    >;
};

TICK_TRAIT(is_advanceable)
{
    template<class T, class Number>
    auto requires_(T&& x, Number n) -> tick::valid<
        decltype(x += n)
    >;
};

Ahora, si escribes las dos sobrecargas podría ser ambiguo. Así que hay un par de maneras de resolver la ambigüedad. En primer lugar, se puede utilizar el envío de etiquetas:

template <typename It>
auto distance(It it, It end, tick::tag<is_incrementable>) -> std::ptrdiff_t 
{
    std::ptrdiff_t size{};
    while (it != end) {
        ++it;
        ++size;
    }
    return size;
}

template <typename It>
auto distance(It begin, It end, tick::tag<is_advanceable>())
{
    return end - begin;
}

template<typename It, TICK_REQUIRES(is_incrementable<It>())>
auto distance(It begin, It end)
{
    return distance(begin, end, tick::most_refined<is_advanceable<It>());
}

Otra forma es usar la sobrecarga condicional proporcionada por la biblioteca Fit. Esto permite su orden de las funciones por importancia con el fin de evitar la ambigüedad. Puede utilizar objetos de función o lambdas. Aquí está cómo hacerlo usando lambdas genérico:

FIT_STATIC_FUNCTION(distance) = fit::conditional(
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_incrementable>(begin) and 
        tick::trait<is_incrementable>(end)))
    {
        std::ptrdiff_t size{};
        while (it != end) {
            ++it;
            ++size;
        }
        return size;
    },
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_advanceable>(begin) and 
        tick::trait<is_advanceable>(end)))
    {
        return end - begin;
    }
);

Por supuesto, esto lo convierte en un objeto de función, que tendría que envolver en una función real si desea confiar en la búsqueda ADL.

Puntos de personalización

Se pueden implementar algoritmos genéricos de tal manera que se pueda agregar una nueva idea de mejora sin cambiar las implementaciones existentes y, si es así, ¿Cómo?

Sí que pueden, pero es necesario definir los puntos de personalización.

Búsqueda ADL

Una forma es a través de la búsqueda de ADL. Las funciones std::begin y std::end funcionan de esta manera. Así que podría definir la función distance en su propio espacio de nombres privado:

namespace detail {
    template<typename It, TICK_REQUIRES(is_incrementable<It>())>
    auto distance(It begin, It end)
    {
        // Implementation of distance
    }
}

Luego puede definir otra función para que el usuario la use en otro espacio de nombres como esta:

namespace my_lib {
template<typename It, TICK_REQUIRES(is_incrementable<It>())>
auto distance(It begin, It end)
{
    using detail::distance;
    distance(begin, end);
}
}

Así que ahora puede personalizar la función distance para ciertos tipos.

Plantilla especialización

Sin embargo, ADL podría ser secuestrado inadvertidamente, y causaría que esto fallara a veces. Así que otra forma de proporcionar puntos de personalización es usar la especialización de plantillas. Así que podrías definir una plantilla que podría usarse para anular el comportamiento de distance, así:

template<class It, class=void>
struct distance_op;

Entonces la función distance podría definirse para preferir primero la distance_op:

 FIT_STATIC_FUNCTION(distance) = fit::conditional(
    [](auto begin, auto end) FIT_RETURNS
    (distance_op<decltype(begin)>::call(begin, end)),
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_incrementable>(begin) and 
        tick::trait<is_incrementable>(end)))
    {
        std::ptrdiff_t size{};
        while (it != end) {
            ++it;
            ++size;
        }
        return size;
    },
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_advanceable>(begin) and 
        tick::trait<is_advanceable>(end)))
    {
        return end - begin;
    }
);

El FIT_RETURNS limitará la lambda a cuando distance_op<decltype(begin)>::call(begin, end) sea válida. Así que si quieres personalizar distance para std::queue, podrías escribir:

template<>
struct distance_op<queue<int>::iterator>
{
    static void call(queue<int>::iterator begin, queue<int>::iterator end)
    {
        // Do queue-based distance
    }
};

Además, el segundo parámetro está allí para que pueda especializarse en función de los tipos que coinciden con ciertas restricciones, por lo que podríamos implementarlo para cada iteratar donde is_queue_iterator es verdadero, así:

template<Iterator>
struct distance_op<Iterator, TICK_CLASS_REQUIRES(is_queue_iterator<Iterator>())>
{
    static void call(queue<int>::iterator begin, queue<int>::iterator end)
    {
        // Do queue-based distance
    }
};

Mapas conceptuales

Seguimiento opcional (estoy principalmente interesado en la pregunta anterior, pero esta también podría ser interesante): Si eso no es posible, ¿se agregaría esta habilidad con conceptos?

Sí, usando mapas conceptuales usted podría extender fácilmente estas operaciones. Así que puedes crear una distancia concept como esta:

template<class Iterator>
concept Distance
{
    ptrdiff_t distance(Iterator begin, Iterator end);
}

, a Continuación, hacemos un concept_map para cuando es un Incrementable y cuando es Advanceable:

template<Incrementable Iterator>
concept_map Distance<Iterator>
{
    ptrdiff_t distance(Iterator begin, Iterator end)
    {
        std::ptrdiff_t size{};
        while (it != end) {
            ++it;
            ++size;
        }
        return size;
    }
};

template<Advanceable Iterator>
concept_map Distance<Iterator>
{
    ptrdiff_t distance(Iterator begin, Iterator end)
    {
        return end - begin;
    }
};

Y luego más tarde el usuario podría especializar el concept_map para nuevos tipos también:

template<class T>
concept_map Distance<queue<T>::iterator>
{
    ptrdiff_t distance(Iterator begin, Iterator end)
    {
        return end - begin;
    }
};
 3
Author: Paul Fultz II,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-17 00:19:04

Usaría una clase de utilidad para eso, porque en ese caso es fácil darle un algoritmo predeterminado (para el caso genérico) manteniendo la posibilidad de sobrescribirlo para usos específicos. Más o menos lo que las clases del STL hacen con el Asignador:

template < class T, class Alloc = allocator<T> > class list;

De forma predeterminada, obtiene un allocator<T> pero puede proporcionarle su propia implementación.

template <class T, class Dist = dist<T> >
class dist_measurer {
public:
    static auto distance(T begin, T end) {
        return Dist.distance(begin, end);
    }
}

Luego crea el genérico dist<T>, y opcionalmente otras implementaciones específicas, todas con una sola distancia de método estático.

Cuando desea utilizar el método genérico en la clase X:

dist_measurer<X>.distance(x, y); // x and y objects of class X

Si ha implementado otro algoritmo en dist2, lo usa con:

dist_measurer<X, dist2<X> >.distance(x, y);
 0
Author: Serge Ballesta,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-16 19:22:45