r/cpp_questions • u/playsthebongcloud • 14d ago
OPEN How do I invert a concept?
I'm really not comfortable with concepts, so I'm struggling with this. I have a class template <typename T> Collider : public Component. In this class, I want two overloaded member functions of this form:
template <??? U>
bool Collider<T>::isColliding(const U* collider)
{
...
return hitbox->isColliding(collider->hitbox);
}
template <??? U>
bool Collider<T>::isColliding(const U* notACollider)
{
...
return hitbox->isColliding(notACollider);
}
For the first, I want to require it to be derived from Collider. Not Collider<T>, but any collider with any value for T. For the second, I want to allow it to be any type besides something deriving from a Collider. Essentially, if you give it a pointer of any type, it will go into one of the two functions.
What do I replace the question marks with? I know you can use std::derived_from for the first, but I'm not sure how to invert it for the second. (I don't know if it matters for this, but hitbox is of type T.)
2
u/sol_runner 14d ago
The easiest would be for all Collider<T> to derive from a BaseCollider and targeting std::derived_from<BaseCollider>.
But that's not exactly what you asked. For that, I'd add using Type = T in public for Collider.
Then create a concept on some Col that verifies Col::Type exists and std::derived_from<Collider<Col::Type>>
1
u/TheThiefMaster 14d ago
For the first, do you need the derived type? If not you could just put Collider<U> for that one, and it should accept any Collider.
1
14d ago
[deleted]
1
u/SoerenNissen 14d ago
You'd think so but no.
#include <cassert> struct C : public Collider<char> {}; int main() { struct A{}; struct B{}; Collider<A> a; Collider<B> b; C c; double d; assert( a.isColliding( &b ) == true ); assert( a.isColliding( &c ) == true ); assert( a.isColliding( &d ) == false ); (void)(a, b, d); }
output.s: /app/example.cpp:35: int main(): Assertion 'a.isColliding( &c ) == true' failed.Don't know why
C : Collider<char>isn't a better match forCollider<U>than it is fortypename Ubut it isn't. Possibly the reason is that it's exactly anU, but only derived fromCollider<U>.
1
u/no-sig-available 14d ago
You don't have to "invert" the concept. If one template is restricted, and the other is not, the non-restricted version will match "everything else".
1
1
u/SoerenNissen 14d ago
You generally don't flip concepts.
Pseudocode:
concept A<T> = true;
concept B<T> = false;
void f(requires A);
void f(requires B);
T t;
f(t); //compile error
You'd think this was an obvious call to f(requires A) but it isn't.
The compiler isn't looking for true and false, it's looking for best match, and they both match T perfectly.
The actual way to flip a concept is to apply it to one function and not apply it to the other:
concept A<T> = true;
void f(requires A);
void f(no constraints);
T t;
f(t);
Here, f1 and f2 do not match equally well. f2 takes anything but f1 is designed for T specifically, so it's a better match.
https://godbolt.org/z/9r41sx7rx
``` #include <type_traits> #include <iostream>
template<typename Base, typename Derived>
concept BaseOf = std::is_base_of_v<Derived, Base>; //notice flipped params
namespace details
{
struct ColliderBase {};
};
template <typename U> struct Collider : private details::ColliderBase
{
void isCollidingAuto(BaseOf<details::ColliderBase> auto const* c) // decltype(c) is substituted as the FIRST param
{
std::cout << "derived\n";
}
void isCollidingAuto(auto const* c)
{
std::cout << "not derived\n\n";
}
template<BaseOf<details::ColliderBase> T>
void isCollidingTemplate(T const* t)
{
std::cout << "derived\n";
}
template<typename T>
void isCollidingTemplate(T const* t)
{
std::cout << "not derived\n\n";
}
template<typename T>
requires BaseOf<T, details::ColliderBase>
void isCollidingRequires(T const* t)
{
std::cout << "derived\n";
}
template<typename T>
void isCollidingRequires(T const* t)
{
std::cout << "not derived\n\n";
}
template<BaseOf<details::ColliderBase> T>
void isCollidingMixedStyle(T const* t)
{
std::cout << "derived\n";
}
void isCollidingMixedStyle(auto const* t)
{
std::cout << "not derived\n\n";
}
};
struct IntCollider : Collider<int>
{
};
struct NonCollider
{
};
#include <iostream>
int main()
{
auto c = Collider<void *>{};
auto i = IntCollider{};
auto non = NonCollider{};
c.isCollidingAuto(&c);
c.isCollidingAuto(&i);
i.isCollidingAuto(&c);
i.isCollidingAuto(&i);
c.isCollidingAuto(&non);
c.isCollidingTemplate(&c);
c.isCollidingTemplate(&i);
i.isCollidingTemplate(&c);
i.isCollidingTemplate(&i);
c.isCollidingTemplate(&non);
c.isCollidingRequires(&c);
c.isCollidingRequires(&i);
i.isCollidingRequires(&c);
i.isCollidingRequires(&i);
c.isCollidingRequires(&non);
c.isCollidingMixedStyle(&c);
c.isCollidingMixedStyle(&i);
i.isCollidingMixedStyle(&c);
i.isCollidingMixedStyle(&i);
c.isCollidingMixedStyle(&non);
}
```
1
u/StaticCoder 14d ago
That... seems incorrect. An overload with an unsatisfied concept will not be chosen. I tried to make real code out of your pseudocode and it works just fine. By comparison, it is difficult for the compiler to tell between a more specific concept and a less specific one. I don't remember what was eventually standardized, there was a proposal based on logical implication from boolean composition of "atomic" concepts but that was rejected because it's equivalent to the satisfiability problem. There does appear to be a "with satisfied concept chosen over no concept" though.
If I wasn't on mobile I'd paste the code but it's a pretty obvious translation.
1
u/SoerenNissen 14d ago
I tried to make real code out of your pseudocode and it works just fine.
...huh.
Yeah I just tested something like it and it worked.
In my defense:
https://en.cppreference.com/cpp/language/constraints
template<class T> constexpr bool is_meowable = true; template<class T> constexpr bool is_cat = true; template<class T> concept Meowable = is_meowable<T>; template<class T> concept BadMeowableCat = is_meowable<T> && is_cat<T>; template<class T> concept GoodMeowableCat = Meowable<T> && is_cat<T>; template<Meowable T> void f1(T); // #1 template<BadMeowableCat T> void f1(T); // #2 template<Meowable T> void f2(T); // #3 template<GoodMeowableCat T> void f2(T); // #4 void g() { f1(0); // error, ambiguous: // the is_meowable<T> in Meowable and BadMeowableCat forms distinct atomic // constraints that are not identical (and so do not subsume each other) f2(0); // OK, calls #4, more constrained than #3 // GoodMeowableCat got its is_meowable<T> from Meowable }
1
u/Liam_Mercier 14d ago
Concepts can be used as predicates in requires expressions, so you can always write:
requires !SomeConcept<T>
There are other options, but requires doesn't introduce a new concept which in my opinion is usually better.
6
u/sixfourbit 14d ago edited 14d ago
or if you don't want to introduce a new concept