cppfront
cppfront copied to clipboard
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_if
to iterate overvariant
types to simplifyis
forvariant
, - rewrite
is
overloads to useconcepts
instead oftype_traits
(to utilize concepts subsumption rules), - remove
operator_is
, implementation ofis
for variants now usetype_find_if
, -
is
that is not using argumentx
for inspection returnsstd::true_type
orstd::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_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.
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 constexpr
s)
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
variant
oroptional
. 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.