c++20 default comparison operator is a very convenient feature. But I find it less useful if the class has an empty base class.
The default operator<=> performs lexicographical comparison by
successively comparing the base (left-to-right depth-first) and then
non-static member (in declaration order) subobjects of T to compute
<=>, recursively expanding array members (in order of increasing
subscript), and stopping early when a not-equal result is found
According to the standard, the SComparable won't have an operator<=> if base doesn't have an operator<=>. In my opinion it's pointless to define comparison operators for empty classes. So the default comparison operators won't work for classes with an empty base class.
struct base {};
struct SComparable: base {
int m_n;
auto operator<=>(SComparable const&) const& = default; // default deleted, clang gives a warning
};
struct SNotComparable: base {
int m_n;
};
If we are desperate to use default comparison operators and therefore define comparison operators for the empty base class base. The other derived class SNotComparable wrongly becomes comparable because of its empty base class base.
struct base {
auto operator<=>(base const&) const& = default;
};
struct SComparable: base {
int m_n;
auto operator<=>(SComparable const&) const& = default;
};
struct SNotComparable: base { // SNotComparable is wrongly comparable!
int m_n;
};
So what is the recommended solution for using default comparison operators for classes with an empty base class?
Edit: Some answers recommend to add default comparison operator in the empty base class and explicitly delete comparison operator in non-comparable derived classes.
If we add default comparison operator to a very commonly used empty base class, suddenly all its non-comparable derived classes are all comparable (always return std::strong_ordering::equal). We have to find all these derived non-comparable classes and explicitly delete their comparison operators. If we missed some class and later want to make it comparable but forget to customize its comparison operator (we all make mistakes), we get a wrong result instead of a compile error from not having default comparison operator in the empty base as before. Then why do I use default comparison operator in the first place? I would like to save some efforts instead of introducing more.
struct base {
auto operator<=>(base const&) const& = default;
};
struct SComparable: base {
int m_n;
auto operator<=>(SComparable const&) const& = default;
};
struct SNotComparable1: base {
int m_n;
auto operator<=>(SNotComparable1 const&) const& = delete;
};
struct SNotComparableN: base {
int m_n;
// oops, forget to delete the comparison operator!
// if later we want to make this class comparable but forget to customize comparison operator, we get a wrong result instead of a non-comparable compile error.
};
In my opinion it's pointless to define comparison operators for empty classes.
Well, it's clearly not pointless. If what you want to do is default your type's comparisons, that necessarily implies comparing all of your type's subobjects, including the base class subobjects, which requires them to be comparable - even if they're empty.
What you need to do is provide them - just conditionally. The simplest way of doing so is probably to provide a different empty base class:
struct base { /* ... */ };
struct comparable_base : base {
friend constexpr auto operator==(comparable_base, comparable_base)
-> bool
{
return true;
}
friend constexpr auto operator<=>(comparable_base, comparable_base)
-> std::strong_ordering
{
return std::strong_ordering::equal;
}
};
And then inherit from comparable_base when you want to have comparisons, and base when you don't. That is:
struct SComparable: comparable_base {
int m_n;
auto operator<=>(SComparable const&) const& = default;
};
struct SNotComparable: base {
int m_n;
};
I'm using hidden friend comparisons there just to be able to take the type by value - since it's empty. Could just as easily be a member function too.
what is the recommended solution for using default comparison operators for classes with an empty base class?
The solution is to add the default comparator to the base class and then do what you do in SComparable if you want the added member(s) of SComparable to be included in the comparison - just as with a base class with members.
If you don't want them to be included in the comparison, don't add a default comparator, like you do in SNotComparable - and the base class comparator will be used - again, just like in a base class with members.
If you don't want the base class behavior in SNotComparable and you don't want SNotComparable to be comparable, then delete the comparator, just like you would if the base class had members:
auto operator<=>(SNotComparable const&) const& = delete;
I'd like to make a small modification based on #Barry's answer. We could have a generic mix-in class comparable<EmptyBase> that provides comparable operators for any empty base. If we want to use default comparison operators for a class derived from empty base class(es), we can simple derive such class from comparable<base> instead of base. It also works for chained empty bases comparable<base1<base2>>.
struct base { /* ... */ };
template<typename EmptyBase>
struct comparable: EmptyBase {
static_assert(std::is_empty<EmptyBase>::value);
template<typename T> requires std::same_as<comparable>
friend constexpr auto operator==(T const&, T const&)
-> bool
{
return true;
}
template<typename T> requires std::same_as<comparable>
friend constexpr auto operator<=>(T const&, T const&)
-> std::strong_ordering
{
return std::strong_ordering::equal;
}
};
struct SComparableDefault: comparable<base> {
int m_n;
auto operator<=>(SComparableDefault const&) const& = default;
};
struct SNotComparable: base {
int m_n;
};
struct SComparableNotDefault: base {
int m_n;
constexpr bool operator==(SComparableNotDefault const& rhs) const& {
/* user defined... */
}
constexpr auto operator<=>(SComparableNotDefault const& rhs) const& {
/* user defined... */
}
};
Related
I've been working on some class hierarchy with a goal to have a collection of classes that encapsulate primitive types (AdvancedInt, AdvanceDouble, etc.) and give them some additional behavior when accessing their underlaying values, through conversion operator and assignment operator. I can't do this with getters and setter as I can't change the original code and the way it access them, so my only choice is operator overloading.
The problem I ran into is with the templated class that defines the conversion operator which gives me a compiler error.
Here is the code that illustrates the problem
#include <string>
class Base {
public:
Base() = delete;
Base(std::string s) : name(s) {
//Some extra logic
}
~Base() {
//More custom logic
}
virtual std::string get_name() const final { //Final on purpose
return name;
}
private:
const std::string name;
};
template <typename T, class C> class Serializable :public Base { //Poor man's interface (but not quite)
public:
Serializable() = delete;
Serializable(std::string name) : Base(name) {
}
operator T() const {
//Some extra 'getter' logic
return value;
}
C& operator=(const T& val) {
//Some extra 'setter' logic
value = val;
return (C&)*this;
}
virtual void serialize() = 0; //This has to be pure virtual
private:
T value;
};
class AdvancedInt final : public Serializable<int, AdvancedInt> { //This is the actual complete class
public:
AdvancedInt(std::string name) : Serializable(name) {
//Nothing here, but needed for the special constructor logic from AbstractBase
}
void serialize() override {
//Some logic here
}
};
int main()
{
AdvancedInt adv{"foo"};
int calc = adv;
calc += 7;
adv = calc; //error C2679 (msvc) | binary '=': no operator found which takes a right-hand operand of type 'int' (or there is no acceptable conversion)
return 0;
}
Now aside from the (probably) questionable practice of passing AdvancedInt to the template of its own base class and it subsequently knowing that it's part of some derived class and implementing its logic (although any feedback on this is welcome), my main question is, what am I doing wrong here?
The C& operator=(const T& val) compiles fine when defined as pure virtual, but that enforces the implementation to be in the derived class, and when implemented there with T and C replaced by their corresponding types it works just fine, so the signature is logically sound, but probably not in the correct place(?). From what I've found on cppreference.com, the simple assignment operator is the only assignment operator that cannot be defined outside of class definition. Is this the reason why this code doesn't compile? And if so, is declaring it as pure virtual a way to go?
With adv = calc we look for operator= from AdvancedInt and found (implicit):
AdvancedInt& operator=(const AdvancedInt&);
AdvancedInt& operator=(AdvancedInt&&);
and look-up stops there.
Adding:
using Serializable<int, AdvancedInt>::operator=;
would allow to consider also that overload.
And that fixes your issue.
Demo.
As per [dcl.init.aggr] I cannot aggregate init a type, if it has (among other things) virtual functions, which includes inheriting from a type with a virtual destructor. However, I'd like to avoid having to write a ton of boilerplate constructors.
MWE:
struct Base {
virtual ~Base() {}
};
struct Derived : Base {
int i;
};
int main() {
Derived d1{42}; // not allowed in this fashion
Derived d2{{},42}; // also not allowed
}
In my setting I have a lot of types like Derived and they are all plain structs with a number of members (not necessarily trivial/pod) except for the fact that they have to inherit from Base.
Is there a way to avoid having to write Derived(int i) : Base(), i(i) {} constructors for all of them?
The one solution I could think of, is to exploit the fact that a struct without the inheritance above will gladly emit a default aggregate initialiser. So I compose that struct together with a templated wrapper type.
template <typename T>
struct BaseWrapper : Base, T {
BaseWrapper(T data) : Node(), T(data) {}
BaseWrapper() = delete;
BaseWrapper(BaseWrapper const&) = default;
BaseWrapper(BaseWrapper&&) = default;
BaseWrapper& operator=(BaseWrapper const&) = default;
BaseWrapper& operator=(BaseWrapper&&) = default;
static T const& cast(Base const& b) {
return static_cast<T const&>(static_cast<BaseWrapper<T> const&>(b));
}
static T& cast(Base& b) {
return static_cast<T&>(static_cast<BaseWrapper<T>&>(b));
}
};
And since I use the Derived types as shared pointers, a small convenience function:
template <typename T, typename... Args>
inline std::shared_ptr<BaseWrapper<T>> make_bw(Args&&... args) {
return std::make_shared<BaseWrapper<T>>(T{std::forward<Args>(args)...});
}
Allows us to create objects without the need of a dedicated constructor inside the object:
struct Derived { // note the missing : Base
int i;
};
auto p = make_bw<Derived>(42);
This is a bit of a cheaty solution, so a proper answer would still be useful.
Is it possible to check, through a base class pointer, whether different derived template classes are specialization of the same template class?
This is achievable through introducing an intermediate non-template base-class. However, i would like to know whether this pattern is avoidable when the sole purpose of this intermediate class is for identification:
class A{}
class B_base : public A{}
template<T>
class B : public B_base {}
// There may be other derived classes of A
template<T>
class C: public A{}
void main() {
// ... some vector of pointers to A derived objects
std::vector<A*> v;
for(auto& i : v){
// Check whether i is any specialization of B through a
// dynamic_cast to the intermediate class
if(dynamic_cast<B_base*>()){
// This is a B_base object,
}
}
}
Ideally, i would like something like this, to avoid the intermediate class.
class A{}
template<T>
class B : public A{}
// There may be other derived classes of A
template<T>
class C: public A{}
void main() {
// ... some vector of pointers to A derived objects
std::vector<A*> v;
for(auto& i : v){
// Check whether i is any specialization of B
if(templateTypeId(i) == templateTypeId(B*)){
// This is a B object with some unknown specialization
}
}
}
Different specializations of a template are entirely unrelated types for most purposes. Template argument deduction can deduce a template and its arguments from such a type, but that happens entirely at compile time. There is no guaranteed run time information that can tell whether a class is a specialization of a given template, whether two classes are specializations of the same template, etc.
So you would need to set up a way to test this yourself, but your intermediate class method is not the only option. The most straightforward way would be to put a way to test it into the base A class:
class A {
public:
virtual ~A() = default;
virtual bool is_B() const noexcept { return false; }
};
template <class T>
class B : public A {
public:
bool is_B() const noexcept override { return true; }
};
Though this gets a bit ugly if there are several different B-like categories to test for, and doesn't work if it should be possible to extend A with new subtypes, and then test for those subtypes in a similar way.
Another idea would be to associate the type check with an object address:
struct type_tag {
constexpr type_tag() = default;
type_tag(const type_tag&) = delete;
type_tag& operator=(const type_tag&) = delete;
};
class A {
public:
virtual ~A() = default;
virtual bool matches_type(const type_tag&) const
{ return false; }
};
inline constexpr type_tag B_tag{};
template <class T>
class B {
public:
bool matches_type(const type_tag& tag) const override
{ return &tag == &B_tag; }
};
This pattern also allows for categories of subtypes that don't come from just one template. It also doesn't prevent a new class from "lying" about its own type, if that might be a concern, but it might be best not to try to prevent that, but let any implemented derived class be responsible for its own behavior, which might mean it wants to act "almost exactly like" some other type.
May be a better design is to add required virtual functions to interface A, so that you can invoke them directly on A* without guessing the derived class. The latter is an anti-pattern because it defeats the purpose of polymorphism: the idea that a piece of code can work with object of different classes without knowing their exact type. You may as well put objects of different types into different containers and not use ploymorphism based on virtual functions at all.
Until a test I've just made, I believed that only Constructors were not inherited in C++. But apparently, the assignment operator= is not too...
What is the reason of that ?
Is there any workaround to inherit the assignment operator ?
Is it also the case for operator+=, operator-=, ... ?
Are all other functions (apart from constructors/operator=) inherited ?
In fact, I encountered this problem as I was doing some CRTP :
template<class Crtp> class Base
{
inline Crtp& operator=(const Base<Crtp>& rhs) {/*SOMETHING*/; return static_cast<Crtp&>(*this);}
};
class Derived1 : public Base<Derived1>
{
};
class Derived2 : public Base<Derived2>
{
};
Is there any solution to get that working ?
EDIT : OK, I have isolated the problem. Why the following isn't working ? How to solve the problem ?
#include <iostream>
#include <type_traits>
// Base class
template<template<typename, unsigned int> class CRTP, typename T, unsigned int N> class Base
{
// Cast to base
public:
inline Base<CRTP, T, N>& operator()()
{
return *this;
}
// Operator =
public:
template<typename T0, class = typename std::enable_if<std::is_convertible<T0, T>::value>::type>
inline CRTP<T, N>& operator=(const T0& rhs)
{
for (unsigned int i = 0; i < N; ++i) {
_data[i] = rhs;
}
return static_cast<CRTP<T, N>&>(*this);
}
// Data members
protected:
T _data[N];
};
// Derived class
template<typename T, unsigned int N> class Derived : public Base<Derived, T, N>
{
};
// Main
int main()
{
Derived<double, 3> x;
x() = 3; // <- This is OK
x = 3; // <- error: no match for 'operator=' in ' x=3 '
return 0;
}
The assignment operator is technically inherited; however, it is always hidden by an explicitly or implicitly defined assignment operator for the derived class (see comments below).
(13.5.3 Assignment) An assignment operator shall be implemented by a
non-static member function with exactly one parameter. Because a copy
assignment operator operator= is implicitly declared for a a class if
not declared by the user, a base class assignment operator is always
hidden by the copy assignment operator of the derived class.
You can implement a dummy assignment operator which simply forwards the call to the base class operator=, like this:
// Derived class
template<typename T, unsigned int N> class Derived : public Base<Derived, T, N>
{
public:
template<typename T0, class = typename std::enable_if<std::is_convertible<T0, T>::value>::type>
inline Derived& operator=(const T0& rhs)
{
return Base<Derived, T, N>::operator=(rhs);
}
};
The assignment operator is inherited, sort of, but... In any given
class, if you do not provide a copy assignment operator, the compiler
generates one for you. That means that your derived classes effectively
have an assignment operator:
Derived& operator=( Derived const& );
And the usual hiding rules apply; this hides all of the base class
assignment operators. (If the base class had an assignment operator
with this signature, the derived class would inherit it normally.)
Your assignment operator is technically inherited, but then it's hidden by the default copy assignment operator in the derived class. This default copy assignment then tries to call the base class's copy assignment which doesn't exist since you hid it with your own assignment.
The sanest way to resolve this is to not use operator overloading in non-obvious ways (= not meaning copy assignment for example). In this case, don't use operator=: Call it something like assign or set and then it will inherit and not be hidden by the child copy assignment.
These operators are inherited and there are no compiler versions so they will never be automatically hidden like operator=.
It really is only constructors that aren't inherited, and I can't think of any other compiler-generated functions that could hide something from the parent as in operator=.
Until a test I've just made, I believed that only Constructors were not inherited in C++. But apparently, the assignment operator= is not too...
What is the reason of that ?
Is there any workaround to inherit the assignment operator ?
Is it also the case for operator+=, operator-=, ... ?
Are all other functions (apart from constructors/operator=) inherited ?
In fact, I encountered this problem as I was doing some CRTP :
template<class Crtp> class Base
{
inline Crtp& operator=(const Base<Crtp>& rhs) {/*SOMETHING*/; return static_cast<Crtp&>(*this);}
};
class Derived1 : public Base<Derived1>
{
};
class Derived2 : public Base<Derived2>
{
};
Is there any solution to get that working ?
EDIT : OK, I have isolated the problem. Why the following isn't working ? How to solve the problem ?
#include <iostream>
#include <type_traits>
// Base class
template<template<typename, unsigned int> class CRTP, typename T, unsigned int N> class Base
{
// Cast to base
public:
inline Base<CRTP, T, N>& operator()()
{
return *this;
}
// Operator =
public:
template<typename T0, class = typename std::enable_if<std::is_convertible<T0, T>::value>::type>
inline CRTP<T, N>& operator=(const T0& rhs)
{
for (unsigned int i = 0; i < N; ++i) {
_data[i] = rhs;
}
return static_cast<CRTP<T, N>&>(*this);
}
// Data members
protected:
T _data[N];
};
// Derived class
template<typename T, unsigned int N> class Derived : public Base<Derived, T, N>
{
};
// Main
int main()
{
Derived<double, 3> x;
x() = 3; // <- This is OK
x = 3; // <- error: no match for 'operator=' in ' x=3 '
return 0;
}
The assignment operator is technically inherited; however, it is always hidden by an explicitly or implicitly defined assignment operator for the derived class (see comments below).
(13.5.3 Assignment) An assignment operator shall be implemented by a
non-static member function with exactly one parameter. Because a copy
assignment operator operator= is implicitly declared for a a class if
not declared by the user, a base class assignment operator is always
hidden by the copy assignment operator of the derived class.
You can implement a dummy assignment operator which simply forwards the call to the base class operator=, like this:
// Derived class
template<typename T, unsigned int N> class Derived : public Base<Derived, T, N>
{
public:
template<typename T0, class = typename std::enable_if<std::is_convertible<T0, T>::value>::type>
inline Derived& operator=(const T0& rhs)
{
return Base<Derived, T, N>::operator=(rhs);
}
};
The assignment operator is inherited, sort of, but... In any given
class, if you do not provide a copy assignment operator, the compiler
generates one for you. That means that your derived classes effectively
have an assignment operator:
Derived& operator=( Derived const& );
And the usual hiding rules apply; this hides all of the base class
assignment operators. (If the base class had an assignment operator
with this signature, the derived class would inherit it normally.)
Your assignment operator is technically inherited, but then it's hidden by the default copy assignment operator in the derived class. This default copy assignment then tries to call the base class's copy assignment which doesn't exist since you hid it with your own assignment.
The sanest way to resolve this is to not use operator overloading in non-obvious ways (= not meaning copy assignment for example). In this case, don't use operator=: Call it something like assign or set and then it will inherit and not be hidden by the child copy assignment.
These operators are inherited and there are no compiler versions so they will never be automatically hidden like operator=.
It really is only constructors that aren't inherited, and I can't think of any other compiler-generated functions that could hide something from the parent as in operator=.