cppfront icon indicating copy to clipboard operation
cppfront copied to clipboard

Rework of `is` that adds new functionalities or simplify implementation

Open filipsajdak opened this issue 1 year ago • 47 comments

This change reworks a big part of the code for is.

  • added concepts for simplify and utilize concept subsumption rules,
    • inspired by @JohelEGP - I have learned more here: https://andreasfertig.blog/2020/09/cpp20-concepts-subsumption-rules/
  • Added type_find_if to iterate over variant types to simplify is for variant,
  • rewrite is overloads to use concepts instead of type_traits (to utilize concepts subsumption rules),
  • remove operator_is, implementation of is for variants now use type_find_if,
  • is that is not using argument x for inspection returns std::true_type or std::false_type
  • add many atomic concepts that were used to build complex concepts - that are required for concept subsumption rules,

Closes #689 Closes #669

Cases handled by the new is():

  • type is type,
  • type is template,
  • type is type_trait,
  • type is concept, (no cpp2 syntax)
  • variable is template,
  • variable is type, (also works for variant, any, optional)
  • variable is value, (also works for variant, any, optional)
  • variable is empty, (also works for variant, any, optional)
  • variable is type_trait,
  • variable is predicate, (works also with free function and generic functions; forbids implicit cast of arguments)

Tests

  • All tests pass,
  • Added tests that present all use cases handled by this change

Issue

  • does not compile on macos-11, clang++, c++20 (one of the CI targets)

The original PR contained a rework of as() and to_string() - they will be provided in separate PRs.

filipsajdak avatar Sep 24 '23 16:09 filipsajdak

  • add possibility to check if object fulfills concept by using x is (:<T:std::integral>() = {}) syntax,

See https://github.com/hsutter/cppfront/issues/575#issuecomment-1676199109:

That isn't yet supported, but my intent is to spell it as <T: type is std::regular>.

JohelEGP avatar Sep 24 '23 16:09 JohelEGP

  • add possibility to check if object fulfills concept by using x is (:<T:std::integral>() = {}) syntax,

See #575 (comment):

That isn't yet supported, but my intent is to spell it as <T: type is std::regular>.

Thank you. @hsutter propose the syntax on cpp2 side. I have prepared the implementation on C++20 side, and the intention is to be able to use it e.g. in if or inspect (assuming <concept> will be syntax for using in such cases - similarly as we are using (value) with comparing to values or to predicates):

inspect x -> std::string {
  is <std::integral> = "x is integral type";
  is <std::floating_point> = "x is floating point type";
  is _ = "other type";
}

filipsajdak avatar Sep 24 '23 17:09 filipsajdak

If <> will be used to disambiguate concepts, shouldn't x is (:<T:std::integral>() = {}) be x is (:<T:<std::integral>>() = {})?

JohelEGP avatar Sep 24 '23 17:09 JohelEGP

I don't know. I know that on cppfront, we don't know if something is a concept or a type (at least, I did not find a way to distinguish that on the C++ side). e.g. in x is T, currently, T can be a type or a template - it will be distinguished on the C++ side using overloads is<T>(x)... I did not find a way to make it work when T is a concept.

filipsajdak avatar Sep 24 '23 17:09 filipsajdak

Yeah, there's no way. You can only specialize it to get a prvalue bool back. You can't use it any other way, not even in a requires-expression.

JohelEGP avatar Sep 24 '23 17:09 JohelEGP

In the cases you mentioned, I guess that we don't need additional <> to distinguish that as type and concept are correct in this context.

filipsajdak avatar Sep 24 '23 17:09 filipsajdak

I think I understand now. You're relying on an accidental feature. In that case, T is the name of a NTTP. The lowered syntax happens to work for Cpp1 terse concept syntax. See my reply to Herb's reply: https://github.com/hsutter/cppfront/issues/575#issuecomment-1676200359.

JohelEGP avatar Sep 24 '23 17:09 JohelEGP

I was interested in making C++ code work. Based on that I was able to call it from cpp2.

filipsajdak avatar Sep 24 '23 18:09 filipsajdak

I was interested in making C++ code work. Based on that I was able to call it from cpp2.

Yeah, the functionality works fine. It works if the lambda was declared with Cpp1, and given :<T> () requires std::integral<T> = {}, too.

JohelEGP avatar Sep 25 '23 00:09 JohelEGP

I found errors in my use of concepts (in terms of benefit from subsumption rules). I will update it soon.

filipsajdak avatar Sep 25 '23 14:09 filipsajdak

inspect x -> std::string {
  is <std::integral> = "x is integral type";
  is <std::floating_point> = "x is floating point type";
  is _ = "other type";
}

Although it's in a different context, note that I'm already using <T> to mean the natural thing, a template template parameter: https://github.com/hsutter/cppfront/pull/603/files#diff-1c3784de2023bbe4a493b11b40cad588b19a1ca2219eaeb42589910b99ea3effR1.

JohelEGP avatar Sep 26 '23 18:09 JohelEGP

inspect x -> std::string {
  is <std::integral> = "x is integral type";
  is <std::floating_point> = "x is floating point type";
  is _ = "other type";
}

Although it's in a different context, note that I'm already using <T> to mean the natural thing, a template template parameter: https://github.com/hsutter/cppfront/pull/603/files#diff-1c3784de2023bbe4a493b11b40cad588b19a1ca2219eaeb42589910b99ea3effR1.

Cool! I will take a look at that.

Meanwhile, I am working on correcting this PR, as I found some things that could be improved...

filipsajdak avatar Sep 26 '23 19:09 filipsajdak

I have reworked a lot during my preparation for the talk on cppcon. After the talk, I have done even more... I am changing this PR to the Draft for a moment as I will clean it up. The current status is that it can handle many more cases and switches to perfect forwarding.

I will be traveling today, so I may manage to clean it up.

filipsajdak avatar Oct 07 '23 13:10 filipsajdak

Regarding many overloads. It is my work-in-progress thing, and I want to clean it up. Having multiple overloads allows us to create methods that will return std::true_type/std::false_type in parallel with methods that return other values.

As we want to allow for the custom operator as, and operator is, I started to worry that having a catch them all method will make that harder. I also want to avoid situations when I need to change old functions when I am adding a new cast.

filipsajdak avatar Oct 07 '23 17:10 filipsajdak

I am preparing a lot of test cases as it is really hard to spot all those errors. I have also found some errors that were silently there but changing the approach made them errors.

filipsajdak avatar Oct 07 '23 18:10 filipsajdak

Having multiple overloads allows us to create methods that will return std::true_type/std::false_type in parallel with methods that return other values.

If a single branch is chosen, there's no chance for ambiguity in the return type.

JohelEGP avatar Oct 07 '23 18:10 JohelEGP

I also want to avoid situations when I need to change old functions when I am adding a new cast.

I think it's easier to add new a branch than a new overload.

JohelEGP avatar Oct 07 '23 18:10 JohelEGP

I will check both styles.

filipsajdak avatar Oct 07 '23 22:10 filipsajdak

Ha! I think I found the way to support is with type_trait:

(@JohelEGP, yes, I followed your approach with if constexprs)

template <template <typename> class C, typename X>
    requires std::derived_from<C<std::remove_cvref_t<X>>, std::true_type>
            || std::derived_from<C<std::remove_cvref_t<X>>, std::false_type>
auto is( X&& ) -> decltype(auto) {
    if constexpr ( 
        C<X&&>::value 
        || C<std::remove_reference_t<X>>::value
        || C<std::remove_cv_t<X>>::value 
        || C<std::remove_cvref_t<X>>::value 
    ) {
        return std::true_type{};        
    } else {
        return std::false_type{};
    }
}

I am not 100% sure if it will work for all cases, but it seems to work for the following cases:

    "type_trait"_test = : () = {
        i  : int       = 42;
        ci : const int = 24;

        expect((i is std::is_const) == false) << "i is std::is_const";
        expect((ci is std::is_const) == true) << "ci is std::is_const";
        expect((ci is std::is_integral) == true);
        expect((ci is std::is_floating_point) == false);

        expect((i is std::is_reference) == true);
        expect((i is std::is_lvalue_reference) == true);
        expect((i is std::is_rvalue_reference) == false);
        expect(((move i) is std::is_rvalue_reference) == true);
        expect((42 is std::is_rvalue_reference) == true);
        expect((i is std::is_signed) == true);
        
        expect((i is std::is_unsigned) == false);
        expect((std::as_const(i) is std::is_unsigned) == false);
        expect(((move i) is std::is_unsigned) == false);
        expect((u8(2) is std::is_unsigned) == true);

        vc: VC = ();
        expect((vc is std::has_virtual_destructor) == true);
        a: A = ();
        expect((a is std::has_virtual_destructor) == false);
    };

filipsajdak avatar Oct 08 '23 09:10 filipsajdak

That's interesting. It works when the base characteristic is std::bool_constant. It doesn't work generally, like for https://en.cppreference.com/w/cpp/meta#Property_queries (e.g. 8 is std::alignment_of<std::string>).

JohelEGP avatar Oct 08 '23 13:10 JohelEGP

It is how is works. It inspects the variable (or type) on the left-hand side.

In theory we could have as to get alignment and is to check it.

std::string as std::alignment_of is 8

But it probably messes with the meaning of as

filipsajdak avatar Oct 08 '23 22:10 filipsajdak

But then maybe better will be to just ask:

std::alignment_of<std::string> is 8

filipsajdak avatar Oct 08 '23 22:10 filipsajdak

Right. It makes sense when you view the second argument as being a special case of a predicate, in this case a type trait predicate. 8 is std::alignment_of_v<std::string> should work just fine.

JohelEGP avatar Oct 08 '23 22:10 JohelEGP

Here's a case where it falls short (https://compiler-explorer.com/z/9WqYhf8e6):

#include <concepts>
#include <type_traits>

template <template <typename> class C, typename X>
    requires std::derived_from<C<std::remove_cvref_t<X>>, std::true_type>
            || std::derived_from<C<std::remove_cvref_t<X>>, std::false_type>
auto is( X&& ) -> decltype(auto) {
    if constexpr ( 
        C<X&&>::value 
        || C<std::remove_reference_t<X>>::value
        || C<std::remove_cv_t<X>>::value 
        || C<std::remove_cvref_t<X>>::value 
    ) {
        return std::true_type{};        
    } else {
        return std::false_type{};
    }
}

static_assert(!std::is_constructible_v<int&>);
int i = 0;
static_assert(decltype(is<std::is_default_constructible>(i))::value);

Both pass. It's unfortunately fragile to dealing with references, and probably const, when the traits are sensible to them. Getting such information from the deduced parameter's type is error prone.

JohelEGP avatar Oct 08 '23 22:10 JohelEGP

But i which is int is default constructible. In this case is inspects the variable.

The behavior could be different when inspecting types.

filipsajdak avatar Oct 08 '23 23:10 filipsajdak

@JohelEGP @hsutter I managed to clean it up enough to start the review. There might be still some issues to be cleaned. What I would like to check is the functionality. The tests pass.

I have some doubts regarding recursive checks of types like variant or optional. To illustrate it:

o : std::optional<std::optional<int>> = 42;

expect((o is 42) == true);
expect((o is 24) == false);
expect((o is :(v) v == 42;) == true); // works thanks to template< class T, class U > constexpr bool operator==( const optional<T>& opt, const U& value);
expect((o is :(v:std::optional<int>) v* == 42;) == true);
expect((o is :(v:int) v == 0;) == false);
expect((o is :(v:int) v == 42;) == true);

// ---
v : std::variant<std::optional<int>, std::variant<int>> = std::optional(42);
expect((v is 42) == true);
expect((v is (std::optional(42))) == true);
expect((v is std::optional<int>) == true);

// ---
v.emplace<1>(24);
expect((v is 24) == true);
expect((v is (std::optional(42))) == false);
expect((v is std::optional<int>) == false);

// ---
o2 : std::optional<std::variant<std::optional<int>, double>> = 112;
expect((o2 is 112) == true);

(I am using boost::ut library for my tests - happy to see it working with cppfront).

filipsajdak avatar Oct 14 '23 15:10 filipsajdak

I will rewrite the commits to be more descriptive but I would like to confirm the functionality first.

filipsajdak avatar Oct 14 '23 15:10 filipsajdak

I have some doubts regarding recursive checks of types like variant or optional. To illustrate it:

What are your doubts?

JohelEGP avatar Oct 14 '23 15:10 JohelEGP

Well...

Asking x is std::variant, what do you expect? If x is std::variant to get true, that is fine. What should happen when x is std::variant<std::variant<std::variant<int, long, std::variant<int>>, int, long>, int, long> that have one of the internal variant initialized?

Currently, it is inspected internally, and if any internal variant is initialized, then the answer is true.

filipsajdak avatar Oct 14 '23 16:10 filipsajdak

And if none of the internal variant is initialized then it returns true as well as x is std::variant itself.

filipsajdak avatar Oct 14 '23 16:10 filipsajdak