web-dev-qa-db-fra.com

void_t "peut implémenter des concepts"?

Je regardais la deuxième partie de CppCon2014 de Walter Brown sur la métaprogrammation des modèles , au cours de laquelle il a discuté des utilisations de son roman void_t<> construction. Lors de sa présentation, Peter Sommerlad lui a posé une question que je n'ai pas bien comprise. (le lien va directement à la question, le code en discussion a eu lieu juste avant)

Demanda Sommerlad

Walter, cela signifie-t-il que nous pouvons réellement mettre en œuvre des concepts légers en ce moment?

à laquelle Walter a répondu

Oh oui! Je l'ai fait ... Il n'a pas tout à fait la même syntaxe.

J'ai cru comprendre que cet échange concernait Concepts Lite. Ce modèle est-il vraiment ce polyvalent? Pour une raison quelconque, je ne le vois pas. Quelqu'un peut-il expliquer (ou esquisser) à quoi pourrait ressembler quelque chose comme ça? Est-ce à peu près enable_if et définissant les traits, ou à quoi faisait référence le questionneur?

Le void_t le modèle est défini comme suit:

template<class ...> using void_t = void;

Il l'utilise ensuite pour détecter si les instructions de type sont bien formées, en l'utilisant pour implémenter le is_copy_assignable trait de type:

//helper type
template<class T>
using copy_assignment_t
= decltype(declval<T&>() = declval<T const&>());

//base case template
template<class T, class=void>
struct is_copy_assignable : std::false_type {};

//SFINAE version only for types where copy_assignment_t<T> is well-formed.
template<class T>
struct is_copy_assignable<T, void_t<copy_assignment_t<T>>> 
: std::is_same<copy_assignment_t<T>,T&> {};

À cause de la conversation, je comprends comment cet exemple fonctionne, mais je ne vois pas comment nous en arriverons à quelque chose comme Concepts Lite.

67
Tim Seguine

Oui, les concepts lite habillent essentiellement SFINAE. De plus, il permet une introspection plus profonde pour permettre une meilleure surcharge. Cependant, cela ne fonctionne que si les prédicats de concept sont définis comme concept bool. La surcharge améliorée ne fonctionne pas avec les prédicats du concept actuel, mais une surcharge conditionnelle peut être utilisée. Voyons comment nous pouvons définir des prédicats, des modèles de contrainte et des fonctions de surcharge en C++ 14. C'est un peu long, mais il explique comment créer tous les outils nécessaires pour y parvenir en C++ 14.

Définition des prédicats

Tout d'abord, il est un peu moche de lire le prédicat avec tous les std::declval Et decltype partout. Au lieu de cela, nous pouvons tirer parti du fait que nous pouvons contraindre une fonction en utilisant un type de fin (d'après le blog d'Eric Niebler ici ), comme ceci:

struct Incrementable
{
    template<class T>
    auto requires_(T&& x) -> decltype(++x);
};

Par conséquent, si ++x N'est pas valide, la fonction membre requires_ N'est pas appelable. Nous pouvons donc créer un trait models qui vérifie simplement si requires_ Est appelable en utilisant void_t:

template<class Concept, class Enable=void>
struct models
: std::false_type
{};

template<class Concept, class... Ts>
struct models<Concept(Ts...), void_t< 
    decltype(std::declval<Concept>().requires_(std::declval<Ts>()...))
>>
: std::true_type
{};

Modèles de contrainte

Donc, lorsque nous voulons contraindre le modèle basé sur le concept, nous devrons toujours utiliser enable_if, Mais nous pouvons utiliser cette macro pour le rendre plus propre:

#define REQUIRES(...) typename std::enable_if<(__VA_ARGS__), int>::type = 0

Nous pouvons donc définir une fonction increment qui est contrainte en fonction du concept Incrementable:

template<class T, REQUIRES(models<Incrementable(T)>())>
void increment(T& x)
{
    ++x;
}

Donc, si nous appelons increment avec quelque chose qui n'est pas Incrementable, nous obtiendrons une erreur comme celle-ci:

test.cpp:23:5: error: no matching function for call to 'incrementable'
    incrementable(f);
    ^~~~~~~~~~~~~
test.cpp:11:19: note: candidate template ignored: disabled by 'enable_if' [with T = foo]
template<class T, REQUIRES(models<Incrementable(T)>())>
                  ^

Fonctions de surcharge

Maintenant, si nous voulons faire une surcharge, nous voulons utiliser la surcharge conditionnelle. Disons que nous voulons créer un std::advance en utilisant des prédicats de concept, nous pourrions le définir comme ceci (pour l'instant nous ignorerons le cas décrémentable):

struct Incrementable
{
    template<class T>
    auto requires_(T&& x) -> decltype(++x);
};

struct Advanceable
{
    template<class T, class I>
    auto requires_(T&& x, I&& i) -> decltype(x += i);
};

template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())>
void advance(Iterator& it, int n)
{
    it += n;
}

template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())>
void advance(Iterator& it, int n)
{
    while (n--) ++it;
}

Cependant, cela provoque une surcharge ambiguë (dans les concepts lite, ce serait toujours une surcharge ambiguë à moins que nous ne modifiions nos prédicats pour faire référence aux autres prédicats dans un concept bool) Lorsqu'il est utilisé avec l'itérateur std::vector. Ce que nous voulons faire, c'est ordonner les appels, ce que nous pouvons faire en utilisant une surcharge conditionnelle. On peut penser à écrire quelque chose comme ça (qui n'est pas valide en C++):

template<class Iterator>
void advance(Iterator& it, int n) if (models<Advanceable(Iterator, int)>())
{
    it += n;
} 
else if (models<Incrementable(Iterator)>())
{
    while (n--) ++it;
}

Donc, si la première fonction n'est pas appelée, elle appellera la fonction suivante. Commençons donc par l'implémenter pour deux fonctions. Nous allons créer une classe appelée basic_conditional Qui accepte deux objets de fonction comme paramètres de modèle:

struct Callable
{
    template<class F, class... Ts>
    auto requires_(F&& f, Ts&&... xs) -> decltype(
        f(std::forward<Ts>(xs)...)
    );
};

template<class F1, class F2>
struct basic_conditional
{
    // We don't need to use a requires clause here because the trailing
    // `decltype` will constrain the template for us.
    template<class... Ts>
    auto operator()(Ts&&... xs) -> decltype(F1()(std::forward<Ts>(xs)...))
    {
        return F1()(std::forward<Ts>(xs)...);
    }
    // Here we add a requires clause to make this function callable only if
    // `F1` is not callable.
    template<class... Ts, REQUIRES(!models<Callable(F1, Ts&&...)>())>
    auto operator()(Ts&&... xs) -> decltype(F2()(std::forward<Ts>(xs)...))
    {
        return F2()(std::forward<Ts>(xs)...);
    }
};

Alors maintenant, cela signifie que nous devons plutôt définir nos fonctions en tant qu'objets fonctions:

struct advance_advanceable
{
    template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())>
    void operator()(Iterator& it, int n) const
    {
        it += n;
    }
};

struct advance_incrementable
{
    template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())>
    void operator()(Iterator& it, int n) const
    {
        while (n--) ++it;
    }
};

static conditional<advance_advanceable, advance_incrementable> advance = {};

Alors maintenant, si nous essayons de l'utiliser avec un std::vector:

std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
auto iterator = v.begin();
advance(iterator, 4);
std::cout << *iterator << std::endl;

Il compilera et imprimera 5.

Cependant, std::advance A en fait trois surcharges, nous pouvons donc utiliser le basic_conditional Pour implémenter conditional qui fonctionne pour n'importe quel nombre de fonctions utilisant la récursivité:

template<class F, class... Fs>
struct conditional : basic_conditional<F, conditional<Fs...>>
{};

template<class F>
struct conditional<F> : F
{};

Donc, maintenant nous pouvons écrire le std::advance Complet comme ceci:

struct Incrementable
{
    template<class T>
    auto requires_(T&& x) -> decltype(++x);
};

struct Decrementable
{
    template<class T>
    auto requires_(T&& x) -> decltype(--x);
};

struct Advanceable
{
    template<class T, class I>
    auto requires_(T&& x, I&& i) -> decltype(x += i);
};

struct advance_advanceable
{
    template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())>
    void operator()(Iterator& it, int n) const
    {
        it += n;
    }
};

struct advance_decrementable
{
    template<class Iterator, REQUIRES(models<Decrementable(Iterator)>())>
    void operator()(Iterator& it, int n) const
    {
        if (n > 0) while (n--) ++it;
        else 
        {
            n *= -1;
            while (n--) --it;
        }
    }
};

struct advance_incrementable
{
    template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())>
    void operator()(Iterator& it, int n) const
    {
        while (n--) ++it;
    }
};

static conditional<advance_advanceable, advance_decrementable, advance_incrementable> advance = {};

Surcharge avec Lambdas

Cependant, en plus, nous pourrions utiliser lambdas pour l'écrire au lieu d'objets fonctionnels qui peuvent aider à le rendre plus propre à écrire. Nous utilisons donc cette macro STATIC_LAMBDA pour construire des lambdas au moment de la compilation:

struct wrapper_factor
{
    template<class F>
    constexpr wrapper<F> operator += (F*)
    {
        return {};
    }
};

struct addr_add
{
    template<class T>
    friend typename std::remove_reference<T>::type *operator+(addr_add, T &&t) 
    {
        return &t;
    }
};

#define STATIC_LAMBDA wrapper_factor() += true ? nullptr : addr_add() + []

Et ajoutez une fonction make_conditional Qui est constexpr:

template<class... Fs>
constexpr conditional<Fs...> make_conditional(Fs...)
{
    return {};
}

Ensuite, nous pouvons maintenant écrire la fonction advance comme ceci:

constexpr const advance = make_conditional(
    STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Advanceable(decltype(it), int)>()))
    {
        it += n;
    },
    STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Decrementable(decltype(it))>()))
    {
        if (n > 0) while (n--) ++it;
        else 
        {
            n *= -1;
            while (n--) --it;
        }
    },
    STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Incrementable(decltype(it))>()))
    {
        while (n--) ++it;
    }
);

Ce qui est un peu plus compact et lisible que l'utilisation des versions d'objet fonction.

De plus, nous pourrions définir une fonction modeled pour réduire la laideur decltype:

template<class Concept, class... Ts>
constexpr auto modeled(Ts&&...)
{
    return models<Concept(Ts...)>();
}

constexpr const advance = make_conditional(
    STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Advanceable>(it, n)))
    {
        it += n;
    },
    STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Decrementable>(it)))
    {
        if (n > 0) while (n--) ++it;
        else 
        {
            n *= -1;
            while (n--) --it;
        }
    },
    STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Incrementable>(it)))
    {
        while (n--) ++it;
    }
);

Enfin, si vous êtes intéressé à utiliser les solutions de bibliothèque existantes (plutôt que de rouler les vôtres comme je l'ai montré). Il y a la bibliothèque Tick qui fournit un cadre pour définir des concepts et des modèles contraignants. Et la bibliothèque Fit peut gérer les fonctions et la surcharge.

116
Paul Fultz II