Trăsătură de tip pentru a verifica dacă 2 tipuri sunt sau nu comparabile între ele (nothrow) (Revizuirea codului, C++, Șablon Meta Programare, C++17)

Yamahari a intrebat.

Am început să scriu niște algoritmi care funcționează pe containere eterogene std::tuple. La un moment dat aveam nevoie de o modalitate de a determina dacă tipurile care sunt conținute de tuple sunt comparabile între ele (și dacă această comparație produce un rezultat care este convertibil într-un tip definit de utilizator), așa că am scris o trăsătură de tip pentru a verifica acest lucru. De asemenea, puteți verifica în mod explicit dacă există un operator de comparație specific pentru cele două tipuri date.

Am vrut doar să primesc niște feedback cu privire la implementare și la posibilele preocupări, probleme etc.

Pentru fiecare trăsătură de tip există, de asemenea, un șablon de variabilă de ajutor, așa cum se întâmplă de obicei cu trăsăturile de tip din biblioteca standard începând cu C++14.

Notă: Acest lucru ar fi putut fi realizat, de asemenea, folosind C++11, dar pentru că folosesc C++17 în algoritmii mei, aș putea la fel de bine să îl folosesc aici. 🙂

#include <type_traits>
#include <utility>

namespace meta
{
    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_equal_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_equal_r
        : std::false_type
    {};

    template<typename T, typename U>
    struct has_equal<T, U, std::void_t<decltype(std::declval<T>() == std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() == std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() == std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_equal<T, U, std::void_t<decltype(std::declval<T>() == std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() == std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() == std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() == std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() == std::declval<U>()), R>)>
    {};

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_not_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_not_equal_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_not_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_not_equal_r
        : std::false_type
    {};

    template<typename T, typename U>
    struct has_not_equal<T, U, std::void_t<decltype(std::declval<T>() != std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_not_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() != std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() != std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_not_equal<T, U, std::void_t<decltype(std::declval<T>() != std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() != std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_not_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() != std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() != std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() != std::declval<U>()), R>)>
    {};

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_less
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_less_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_less
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_less_r
        : std::false_type
    {};

    template<typename T, typename U>
    struct has_less<T, U, std::void_t<decltype(std::declval<T>() < std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_less_r<R, T, U, std::void_t<decltype(std::declval<T>() < std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() < std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_less<T, U, std::void_t<decltype(std::declval<T>() < std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() < std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_less_r<R, T, U, std::void_t<decltype(std::declval<T>() < std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() < std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() < std::declval<U>()), R>)>
    {};    

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_greater
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_greater_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_greater
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_greater_r
        : std::false_type
    {};

    template<typename T, typename U>
    struct has_greater<T, U, std::void_t<decltype(std::declval<T>() > std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_greater_r<R, T, U, std::void_t<decltype(std::declval<T>() > std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() > std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_greater<T, U, std::void_t<decltype(std::declval<T>() > std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() > std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_greater_r<R, T, U, std::void_t<decltype(std::declval<T>() > std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() > std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() > std::declval<U>()), R>)>
    {};

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_less_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_less_equal_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_less_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_less_equal_r
        : std::false_type
    {};    

    template<typename T, typename U>
    struct has_less_equal<T, U, std::void_t<decltype(std::declval<T>() <= std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_less_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() <= std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() <= std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_less_equal<T, U, std::void_t<decltype(std::declval<T>() <= std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() <= std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_less_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() <= std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() <= std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() <= std::declval<U>()), R>)>
    {};    

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_greater_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_greater_equal_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_greater_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_greater_equal_r
        : std::false_type
    {};    

    template<typename T, typename U>
    struct has_greater_equal<T, U, std::void_t<decltype(std::declval<T>() >= std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_greater_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() >= std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() >= std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_greater_equal<T, U, std::void_t<decltype(std::declval<T>() >= std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() >= std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_greater_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() >= std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() >= std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() >= std::declval<U>()), R>)>
    {};

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

Acestea sunt șabloanele variabilelor ajutătoare:

    template<typename T, typename U>
    inline constexpr auto has_equal_v = has_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_equal_r_v = has_equal_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_equal_v = has_nothrow_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_equal_r_v = has_nothrow_equal_r<R, T, U>::value;

   // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U>
    inline constexpr auto has_not_equal_v = has_not_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_not_equal_r_v = has_not_equal_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_not_equal_v = has_nothrow_not_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_not_equal_r_v = has_nothrow_not_equal_r<R, T, U>::value;

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U>
    inline constexpr auto has_less_v = has_less<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_less_r_v = has_less_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_less_v = has_nothrow_less<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_less_r_v = has_nothrow_less_r<R, T, U>::value;

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U>
    inline constexpr auto has_greater_v = has_greater<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_greater_r_v = has_greater_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_greater_v = has_nothrow_greater<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_greater_r_v = has_nothrow_greater_r<R, T, U>::value;

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U>
    inline constexpr auto has_less_equal_v = has_less_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_less_equal_r_v = has_less_equal_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_less_equal_v = has_nothrow_less_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_less_equal_r_v = has_nothrow_less_equal_r<R, T, U>::value;

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U>
    inline constexpr auto has_greater_equal_v = has_greater_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_greater_equal_r_v = has_greater_equal_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_greater_equal_v = has_nothrow_greater_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_greater_equal_r_v = has_nothrow_greater_equal_r<R, T, U>::value;    

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

Și acesta este modelul propriu-zis is_comparable de tip:

    template<typename T, typename U>
    struct is_comparable
        : std::bool_constant<
              std::conjunction_v<
                  has_equal<T, U>,
                  has_not_equal<T, U>,
                  has_less<T, U>,
                  has_greater<T, U>,
                  has_less_equal<T, U>,
                  has_greater_equal<T, U>
              > // std::conjunction_v
          > // std::bool_constant
    {}; // is_comparable

    template<typename R, typename T, typename U>
    struct is_comparable_r
        : std::bool_constant<
              std::conjunction_v<
                  has_equal_r<R, T, U>,
                  has_not_equal_r<R, T, U>,
                  has_less_r<R, T, U>,
                  has_greater_r<R, T, U>,
                  has_less_equal_r<R, T, U>,
                  has_greater_equal_r<R, T, U>
              > // std::conjunction_v
          > // std::bool_constant
    {}; // is_comparable_r

    template<typename T, typename U>
    struct is_nothrow_comparable
        : std::bool_constant<
              std::conjunction_v<
                  has_nothrow_equal<T, U>,
                  has_nothrow_not_equal<T, U>,
                  has_nothrow_less<T, U>,
                  has_nothrow_greater<T, U>,
                  has_nothrow_less_equal<T, U>,
                  has_nothrow_greater_equal<T, U>
              > // std::conjunction_v
          > // std::bool_constant
    {}; // is_nothrow_comparable

    template<typename R, typename T, typename U>
    struct is_nothrow_comparable_r
        : std::bool_constant<
              std::conjunction_v<
                  has_nothrow_equal_r<R, T, U>,
                  has_nothrow_not_equal_r<R, T, U>,
                  has_nothrow_less_r<R, T, U>,
                  has_nothrow_greater_r<R, T, U>,
                  has_nothrow_less_equal_r<R, T, U>,
                  has_nothrow_greater_equal_r<R, T, U>
              > // std::conjunction_v
          > // std::bool_constant
    {}; // is_nothrow_comparable_r

    template<typename T, typename U>
    inline constexpr auto is_comparable_v = is_comparable<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto is_comparable_r_v = is_comparable_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto is_nothrow_comparable_v = is_nothrow_comparable<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto is_nothrow_comparable_r_v = is_nothrow_comparable_r<R, T, U>::value;    

} // namespace meta

1 răspunsuri
Quuxplusone

Totul pare plauzibil pentru mine. SFINAE este suficient de concisă.


Din punct de vedere stilistic, acesta este un caz în care aș prefera un număr mic de rânduri masiv de lungi decât un număr mare de rânduri mai scurte. Fiecare dintre cazurile dvs. ocupă patru rânduri de diferite lungimi, ceea ce face dificilă o vizualizare rapidă și dificilă verificarea locurilor în care ați fi putut face o eroare de tăiere și lipire.


template<typename R, typename T, typename U>
    struct has_nothrow_greater_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() >= std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() >= std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() >= std::declval<U>()), R>)>
    {};

Aici verificați că T >= U este convertibil în R, , și că T >= U nu este o linie, dar nu verificați niciodată dacă static_cast<R>(T >= U) nu este nici unthrow. Este intenționat, este o eroare sau un bug, sau nu contează?


template<typename T, typename U>
struct is_nothrow_comparable
    : std::bool_constant<
          std::conjunction_v<
              has_nothrow_equal<T, U>,
              has_nothrow_not_equal<T, U>,
              has_nothrow_less<T, U>,
              has_nothrow_greater<T, U>,
              has_nothrow_less_equal<T, U>,
              has_nothrow_greater_equal<T, U>
          > // std::conjunction_v
      > // std::bool_constant
{}; // is_nothrow_comparable

template<typename T, typename U>
inline constexpr auto is_comparable_v = is_comparable<T, U>::value;

Aș fi înclinat să scriu acest lucru în sens invers, pentru a evita std::conjunction:

template<typename T, typename U>
inline constexpr bool is_comparable_v = 
    has_nothrow_equal_v<T, U> && has_nothrow_not_equal_v<T, U> &&
    has_nothrow_less_v<T, U> && has_nothrow_less_equal_v<T, U> &&
    has_nothrow_greater_v<T, U> && has_nothrow_greater_equal_v<T, U>;

template<typename T, typename U>
struct is_comparable : std::bool_constant<is_comparable_v<T, U>> {};

Dar recunosc majoritatea diferența de stil se află în spațiile albe. 🙂


typename = std::void_t<> este un mod inutil de a scrie typename = void.


Faptul că has_equal ia trei parametri de șablon (unul dintre ei fiind implicit void) mă face să mă simt vag neliniștit. Atunci când biblioteca standard face trăsături de tip, se asigură întotdeauna că punctele de intrare standard iau exact numărul corect de parametri, fără valori implicite:

template<class T, class U, class> struct has_equal_impl                                                                 : std::false_type {};
template<class T, class U>        struct has_equal_impl<T, U, decltype(std::declval<T>() == std::declval<U>(), void())> : std::true_type {};

template<class T, class U> struct has_equal : has_equal_impl<T, U, void> {};

Nu știu dacă există un caz de utilizare specific pentru care acest lucru este bun, dacă este o paranoia a furnizorului de bibliotecă sau dacă este complet inutil. (Lăsați un comentariu dacă știți!)


EDIT: În comentarii, mi-am dat seama că

static_assert(has_equal_r<std::string, std::string, bool>::value);

eșuează, deoarece șablonul își așteaptă parametrii în ordinea neobișnuită „destination-type, operands” în loc de ordinea obișnuită „operands, destination-type” (cf. is_convertible, , Sintaxa Concepts Lite, auto sintaxa de declarare a funcțiilor). Recomand cu tărie inversarea ordinii operanzilor.

În plus, dacă faceți acest lucru, puteți să prăbușiți _r versiuni în celelalte: has_equal<T, U, R=bool>. „Are un == convertibil în bool” nu este chiar aceeași noțiune din punct de vedere semantic ca „Are un ==„; dar diferența apare doar în contexte complet generice. O altă opțiune (care necesită și mai multă metaprogramare, din păcate) este de a folosi cazul special has_equal<T, U, void> ca însemnând „are == de orice tip”. (Spun „necesită mai multă metaprogramare” pentru că, din anumite motive is_convertible<R, void> este de obicei falsă).


Un alt comentariu de ultimă oră pe care am uitat să îl scriu data trecută: Utilizarea sufixului _r mă deranjează. Sunt familiarizat cu _t și _v (și am avut ocazia să folosesc _f pe MSVC, care acceptă funcții constexpr inline, dar nu și variabile constexpr inline), dar _r m-a derutat până când am văzut că îl foloseai pentru a însemna literalmente „acest șablon ia un parametru suplimentar numit _r„. Acesta nu este un motiv bun pentru a folosi un astfel de sufix cu un singur caracter.

glibc folosește, de asemenea, și _r pentru a însemna „reentrant-safe”, ca în qsort_r și strtok_r.

Dacă se reduce _r în versiuni cu două argumente, așa cum am sugerat la punctul anterior, atunci nu va trebui să vă faceți griji cu privire la denumirea versiunilor _r versiuni, astfel încât veți rezolva două probleme deodată. 🙂

Comentarii

  • Vă mulțumim că ați semnalat lipsa static_cast. Problema este următoarea, static_cast îmi dă o eroare compiletime în loc de o eroare std::bool_constant<false> dacă tipul nu este convertibil. Poate că există o soluție de rezolvare, voi căuta asta mai târziu.  > Por Yamahari.
  • Funcționează bine pentru mine; bănuiesc că ai o greșeală de tastare sau de tăiere și lipire undeva. Apropo, nu am observat decât după ce mi-am scris cazurile de test că luați argumentele în ordinea <R,T,U> în loc de cea așteptată de mine <T,U,R>; voi edita răspunsul meu pentru a include acest lucru.  > Por Quuxplusone.
  • Da, am avut niște erori de gândire, de la static_castfuncționează bine. 🙂 Referitor la editarea ta: std::is_invocable. Acesta este motivul pentru care am pus și parametrii șablonului în ordinea în care sunt acum.-  > Por Yamahari.
  • Referitor la std::is_invocable: Mulțumesc, nu știam de asta _r! Dar aplicați doar argumentul meu în amonte la comitetul C++: că _r este, de asemenea, o prostie. Ceea ce ar fi trebuit să facă ar fi fost să adauge un std::invoke_result_t<F, Args...> care să redea tipul real al rezultatului, iar apoi std::is_convertible_v<that, R> v-ar fi dat răspunsul pe care îl căutați. (Oh, așteptați. Ei au făcut-o făcut asta. Deci, acest is_invocable_r_v lucru este o confuzie 100% superfluă care ar fi trebuit să fie exclusă din standard.)- –  > Por Quuxplusone.