This is an archive of the discontinued LLVM Phabricator instance.

[libcxx] adds common_reference to <type_traits>
ClosedPublic

Authored by cjdb on Feb 13 2021, 4:44 PM.

Details

Summary

Implements part of P0898R3 Standard Library Concepts

Reworks D74351 to use requires-clauses over SFINAE and so that it more
closely follows the wording.

Co-authored by: Michael Schellenberger Costa <mschellenbergercosta@googlemail.com>

(Michael did all the heavy lifting and I came in to polish it for
submission, since Michael is focussing on std::format now.)

Diff Detail

Event Timeline

cjdb requested review of this revision.Feb 13 2021, 4:44 PM
cjdb created this revision.
Herald added a project: Restricted Project. · View Herald TranscriptFeb 13 2021, 4:44 PM
Herald added a reviewer: Restricted Project. · View Herald Transcript
cjdb added a comment.Feb 14 2021, 7:31 PM

I'm not sure why Mac OS is failing (and I don't have a Mac to test on at home). It looks like C++20 mode isn't being enabled despite -std=c++2a being in the build config?

rarutyun added inline comments.
libcxx/test/std/utilities/meta/meta.trans/meta.trans.other/common_reference.compile.pass.cpp
23–29

I think you don't need the specialization of template variable. Since requires expression is already constexpr bool semantically, you may use it directly to initialize constexpr bool variable and you don't need requires-clause

template <class T>
constexpr bool is_trait = requires { typename T::type; };

Proof: https://godbolt.org/z/fjjqeE. Please see the program output for two different compilers depending on defined macro

Small naming suggestion. I would rename is_trait to has_type. It explicitly says what you check without looking deeply into the logic how it's determined. But it's up to you. And I would add _v suffix (nevermind if you rename it or not) to just emphasize that it's value rather than the type

I'm not sure why Mac OS is failing (and I don't have a Mac to test on at home). It looks like C++20 mode isn't being enabled despite -std=c++2a being in the build config?

Apple clang doesn't concepts support yet.

libcxx/test/std/utilities/meta/meta.trans/meta.trans.other/common_reference.compile.pass.cpp
11

Make the test unsupported if concepts support is lacking.

cjdb updated this revision to Diff 324175.Feb 16 2021, 8:38 PM

fixes test so it works on compilers without concepts, applies various bits of feedback

cjdb marked an inline comment as done.Feb 16 2021, 8:44 PM
cjdb added inline comments.
libcxx/test/std/utilities/meta/meta.trans/meta.trans.other/common_reference.compile.pass.cpp
11

Thanks, this one was really driving me round the twist.

23–29

I think you don't need the specialization of template variable. Since requires expression is already constexpr bool semantically, you may use it directly to initialize constexpr bool variable and you don't need requires-clause

Good point! Done.

Small naming suggestion. I would rename is_trait to has_type. It explicitly says what you check without looking deeply into the logic how it's determined. But it's up to you.

Agreed, done.

And I would add _v suffix (nevermind if you rename it or not) to just emphasize that it's value rather than the type

The name already does this in both cases, so I don't think it's necessary.

miscco added inline comments.Feb 19 2021, 9:56 AM
libcxx/include/type_traits
2471

I think it would not hurt to add the standardese for X and Y too

2492

Is there a reason for the deep identation?

2494

I think we should go with _Xp and _Yp here

2508

Same would use _Xp and _Yp

cjdb updated this revision to Diff 325374.Feb 21 2021, 10:55 PM
cjdb marked 4 inline comments as done.
cjdb edited the summary of this revision. (Show Details)

applies @miscco's feedback

libcxx/include/type_traits
2471

Good point!

2492

This was copied from D74351. If you don't remember why, I'll delete it (and all the others too?).

ldionne requested changes to this revision.Feb 25 2021, 9:58 AM

Thanks a lot for working on this (both you and @miscco). I left some comments - some of them are about re-organization and could be seen as being subjective. If you disagree with some of my suggestions, let me know and we can talk.

Just for the sake of curiosity, I did try to re-implement COMMON-REF from the spec because I was struggling to get an understanding of what that beast does (and hence give a meaningful review). Here's what I came up with. I am *not* suggesting that we go down that route, I'm just posting it so it doesn't get lost forever since I think it's an interesting take on the problem (using if constexpr). I think your solution is superior though.

template <class _Tp>
struct __type { using type = _Tp; };

struct __illformed;

template <class _Tp>
constexpr bool __is_well_formed_v = !is_same_v<_Tp, __illformed>;

// __maybe_t applies an alias to the provided arguments, and returns
// `__illformed` if the alias isn't well-formed with those arguments.
template <template <class ...> class _Fp, class ..._Args>
struct __maybe {
    using type = __illformed;
};

template <template <class ...> class _Fp, class ..._Args>
    requires requires { typename _Fp<_Args...>; }
struct __maybe<_Fp, _Args...> {
    using type = _Fp<_Args...>;
};

template <template <class ...> class _Fp, class ..._Args>
using __maybe_t = typename __maybe<_Fp, _Args...>::type;


template <class _Xp, class _Yp>
struct __cond_res_criterion {
    using _Res = __maybe_t<__cond_res, __copy_cv<_Xp, _Yp>&, __copy_cv<_Yp, _Xp>&>;
    static constexpr bool value = __is_well_formed_v<_Res> && is_reference_v<_Res>;
};

template <class _Ap, class _Bp, class _Xp, class _Yp>
struct _Cp_criterion;

template <class _Ap, class _Bp, class _Xp, class _Yp>
struct _Dp_criterion;

template <class _Ap, class _Bp>
auto __common_ref_impl() {
    using _Xp = remove_reference_t<_Ap>;
    using _Yp = remove_reference_t<_Bp>;

    // If A and B are both lvalue reference types, COMMON-REF(A, B) is
    // COND-RES(COPYCV(X, Y) &, COPYCV(​Y, X) &) if that type exists and
    // is a reference type.
    if constexpr (_And<is_lvalue_reference<_Ap>,
                       is_lvalue_reference<_Bp>,
                       __cond_res_criterion<_Xp, _Yp>>::value) {
        using _Res = typename __cond_res_criterion<_Xp, _Yp>::_Res;
        return __type<_Res>{};
    }

    // Otherwise, let C be remove_­reference_­t<COMMON-REF(X&, Y&)>&&.
    // If A and B are both rvalue reference types,
    //  C is well-formed,
    //  and is_­convertible_­v<A, C> && is_­convertible_­v<B, C> is true,
    //  then COMMON-REF(A, B) is C.
    else if constexpr (_And<is_rvalue_reference<_Ap>,
                            is_rvalue_reference<_Bp>,
                            _Cp_criterion<_Ap, _Bp, _Xp, _Yp>>::value) {
        using _Cp = typename _Cp_criterion<_Ap, _Bp, _Xp, _Yp>::_Cp;
        return __type<_Cp>{};
    }

    // Otherwise, let D be COMMON-REF(const X&, Y&).
    // If A is an rvalue reference
    //  and B is an lvalue reference
    //  and D is well-formed
    //  and is_­convertible_­v<A, D> is true,
    //  then COMMON-REF(A, B) is D.
    else if constexpr (_And<is_rvalue_reference<_Ap>,
                            is_lvalue_reference<_Bp>,
                            _Dp_criterion<_Ap, _Bp, _Xp, _Yp>>::value) {
        using _Dp = typename _Dp_criterion<_Ap, _Bp, _Xp, _Yp>::_Dp;
        return __type<_Dp>{};
    }

    // Otherwise, if A is an lvalue reference and B is an rvalue reference,
    // then COMMON-REF(A, B) is COMMON-REF(B, A).
    else if constexpr (is_lvalue_reference_v<_Ap> && is_rvalue_reference_v<_Bp>) {
        return __common_ref_impl<_Bp, _Ap>();
    }

    // Otherwise, COMMON-REF(A, B) is ill-formed.
    else {
        return __type<__illformed>{};
    }
}

template <class _Ap, class _Bp, class _Xp, class _Yp>
struct _Cp_criterion {
    using _Cp = remove_reference_t<typename decltype(__common_ref_impl<_Xp&, _Yp&>())::type>&&;
    static constexpr bool value = __is_well_formed_v<_Cp> &&
                                  is_convertible_v<_Ap, _Cp> &&
                                  is_convertible_v<_Bp, _Cp>;
};

template <class _Ap, class _Bp, class _Xp, class _Yp>
struct _Dp_criterion {
    using _Dp = typename decltype(__common_ref_impl<const _Xp&, _Yp&>())::type;
    static constexpr bool value = __is_well_formed_v<_Dp> && is_convertible_v<_Ap, _Dp>;
};

template <class _Ap, class _Bp, class _Res = typename decltype(__common_ref_impl<_Ap, _Bp>())::type>
using __common_ref = enable_if_t<__is_well_formed_v<_Res>, _Res>;
libcxx/include/type_traits
2415

_LIBCPP_HAS_NO_CONCEPTS?

2448

I think it would be a bit easier to follow if you followed the convention of using _t for aliases.

Also, this could be defined outside of the block for common_reference since it's a useful utility on its own. You could also add __copy_cvref, and use that to (trivially) implement __xref below. What do you think?

2483

I don't understand the need for this requires-clause, can you explain?

2494

Do you think it would be possible to paste the whole wording instead of skipping it with ...? It doesn't add much text and it makes it easier to follow along (IMO). LMK if you disagree.

2501–2502

The spec says "If A is an rvalue reference and B is an rvalue reference`, and then requires that is_convertible<A, C> && is_convertible<B, C>. Here, you're taking the reference-ness off of A and B by matching your partial specialization. So I think you need to do is_convertible_v<_Ap&&, __common_ref_C<_Xp, _Yp>>, and similarly for _Bp.

  1. Am I right? Is this a bug or am I confused?
  2. If this is indeed straying from the spec, does it change the result we have in any case?
  3. If so, is it possible to add a test that would catch this?
2515

You have the same problem as above here.

2521

The indentation is a bit strange - is that on purpose?

2528–2530

I think I would do this instead, as it makes the intent a bit clearer IMO:

template <class...>
struct common_reference; // don't define it

// bullet 1 - sizeof...(T) == 0
template <>
struct common_reference<> { };
2537

General note: I don't think we actually need to use _LIBCPP_TEMPLATE_VIS here. I know we do in other places, but I don't think they are necessary anywhere in type_traits.

2544–2582

Another way to string this would be:

// bullet 3 - sizeof...(T) == 2
template <class _Tp, class _Up> struct __common_reference_sub_bullet3;
template <class _Tp, class _Up> struct __common_reference_sub_bullet2 : __common_reference_sub_bullet3<_Tp, _Up> { };
template <class _Tp, class _Up> struct __common_reference_sub_bullet1 : __common_reference_sub_bullet2<_Tp, _Up> { };

// sub-bullet 1 - If T1 and T2 are reference types and COMMON_REF(T1, T2) is well-formed [...]
template <class _Tp, class _Up>
    requires is_reference_v<_Tp> && is_reference_v<_Up> && 
    requires { typename __common_ref<_Tp, _Up>; }
struct __common_reference_sub_bullet1<_Tp, _Up>
{
    using type = __common_ref<_Tp, _Up>;
};

... do other sub bullets in order ...

I think this makes it a bit easier to follow since it highlights that we're basically reimplementing short-circuiting ifs using templates (by making the chain between the various bullets obvious up there). Re-organizing the code this way also allows you to follow the bullets in the order they are in the text instead of in reverse order.

2602

And here you can add the last sub-bullet (http://eel.is/c++draft/meta.trans.other#6.4.2) by defining the base template to be empty:

template <typename ...>
struct common_reference { ];
This revision now requires changes to proceed.Feb 25 2021, 9:58 AM
cjdb updated this revision to Diff 326578.Feb 25 2021, 6:37 PM
cjdb marked 9 inline comments as done.

applies @ldionne's feedback

cjdb added a comment.Feb 25 2021, 6:38 PM

Thanks for your review here, it's really appreciated!

libcxx/include/type_traits
2415

Bleh, I need to merge that patch first. That's going through CI right now, will probably update tomorrow morning (UTC-8).

2448

I think it would be a bit easier to follow if you followed the convention of using _t for aliases.

Done.

Also, this could be defined outside of the block for common_reference since it's a useful utility on its own.

Done.

You could also add copy_cvref, and use that to (trivially) implement xref below.

Isn't copy_cvref just another spelling of xref? Or is that your point? :-)

2483

I had a bug somewhere because I accidentally passed in a reference instead of plain old T IIRC. I wanted some guard rails after that.

2494

Sure. LMK if you want this for std::ranges::swap too (not a part of this review).

2501–2502

Am I right? Is this a bug or am I confused?

This is a bug. Fixed.

If this is indeed straying from the spec, does it change the result we have in any case?
If so, is it possible to add a test that would catch this?

I haven't been able to produce a case where _Ap and _Ap&& disagree.

2521

@miscco is the person to answer that. Now that the full passage is there, I've de-indented it a fair bit.

2537

What's this macro for?

2537

Removed.

2544–2582

This is great, thanks!

2602

Do we want { } or { using type = __ill_formed; }? Which is clearer for (a) the user and (b) maintainers?

cjdb updated this revision to Diff 326847.Feb 26 2021, 4:56 PM

fixes include guards

cjdb marked an inline comment as done.Feb 26 2021, 4:57 PM
ldionne requested changes to this revision.Mar 2 2021, 7:54 AM

This basically LGTM, except for a few comments and the failing CI.

libcxx/include/type_traits
2448

Sorry, that wasn't clear, but what I meant was:

template<class _From, class _To>
struct __copy_cvref { using type = ...; };

template<class _From, class _To>
using __copy_cvref_t = typename __copy_cvref<_From, _To>::type;


// ...
// [Note: `XREF(A)` is `__xref<A>::template __res_type`]
template <class _Tp>
struct __xref {
  template <class _Up>
  using __res_type = __copy_cvref_t<_Tp, _Up>;
};
2537

The macro expands to __attribute__((__visibility__("default"))), which basically makes sure that the RTTI and member functions of the class have default visibility. That shouldn't be necessary for type traits unless I'm missing something.

2602

I think it definitely *needs* to be { } (i.e. no typedef type is provided), otherwise we're not conforming? The way you have it right now seems right to me.

2615

This comment looks wrong to me. I think this should be http://eel.is/c++draft/meta.trans.other#6.4.1 instead?

This revision now requires changes to proceed.Mar 2 2021, 7:54 AM
cjdb updated this revision to Diff 327486.Mar 2 2021, 9:27 AM
cjdb marked 3 inline comments as done.

tries to get CI passing

cjdb added a comment.Mar 2 2021, 9:29 AM

This basically LGTM, except for a few comments and the failing CI.

Here are the unexpected failures I get when running ninja check-cxx locally.

Failed Tests (3):
  libc++ :: libcxx/gdb/gdb_pretty_printer_test.sh.cpp
  libc++ :: libcxx/modules/stds_include.sh.cpp
  libc++ :: std/iterators/iterator.primitives/iterator.traits/empty.fail.cpp

Could these be impacting CI failure? I can't see how common_reference would interact with any of these to the point of failure.

libcxx/include/type_traits
2448

I'm not convinced that's any different to what's currently present, except that your __xref is its own type, whereas mine's just an alias for __copy_cvref.

2537

Thanks, this is good to know :-)

cjdb updated this revision to Diff 327491.Mar 2 2021, 9:44 AM

per offline discussion with @ldionne, I've fixed up __xref

cjdb updated this revision to Diff 327494.Mar 2 2021, 9:49 AM

forgot to test before uploading (sorry)

cjdb updated this revision to Diff 327532.Mar 2 2021, 11:30 AM
cjdb edited the summary of this revision. (Show Details)

gets CI working

cjdb updated this revision to Diff 327536.Mar 2 2021, 11:58 AM

updates copy_cv and copy_cvref

ldionne accepted this revision.Mar 2 2021, 12:04 PM

This looks great to me! Thanks a lot both!

Ship it once CI is green!

This revision is now accepted and ready to land.Mar 2 2021, 12:04 PM
This revision was automatically updated to reflect the committed changes.