Rework of `is` that adds new functionalities or simplify implementation
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_ifto iterate overvarianttypes to simplifyisforvariant, - rewrite
isoverloads to useconceptsinstead oftype_traits(to utilize concepts subsumption rules), - remove
operator_is, implementation ofisfor variants now usetype_find_if, -
isthat is not using argumentxfor inspection returnsstd::true_typeorstd::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.
- 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>.
- 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";
}
If <> will be used to disambiguate concepts, shouldn't
x is (:<T:std::integral>() = {}) be
x is (:<T:<std::integral>>() = {})?
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.
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.
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.
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.
I was interested in making C++ code work. Based on that I was able to call it from cpp2.
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.
I found errors in my use of concepts (in terms of benefit from subsumption rules). I will update it soon.
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.
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...
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.
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.
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.
Having multiple overloads allows us to create methods that will return
std::true_type/std::false_typein parallel with methods that return other values.
If a single branch is chosen, there's no chance for ambiguity in the return type.
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.
I will check both styles.
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);
};
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>).
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
But then maybe better will be to just ask:
std::alignment_of<std::string> is 8
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.
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.
But i which is int is default constructible. In this case is inspects the variable.
The behavior could be different when inspecting types.
@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).
I will rewrite the commits to be more descriptive but I would like to confirm the functionality first.
I have some doubts regarding recursive checks of types like
variantoroptional. To illustrate it:
What are your doubts?
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.
And if none of the internal variant is initialized then it returns true as well as x is std::variant itself.