Spaceship operator with pimpl idiom

116 views Asked by At

I have a class that uses the pimpl idiom.

class MyImpl;

class MyClass
{
public:

private:
    MyImpl* _impl;
};

Now I'd like to add spaceship operator support to this class. Ideally I would write something like:

auto operator<=>(const MyClass& rhs) const
{
    return *_impl <=> *rhs._impl;
}

The implementation is not known in the header file though. Hence, I have to write the implementation in the source file. But that prevents the usage of auto as a return type. I have to be explicit on whether it's std::strong_ordering, std::weak_ordering or std::partial_ordering. But that heavily depends on the details of MyImpl. If MyImpl contains only ints, the return type would be std::strong_ordering, but if it would contain floats too it would be std::partial_ordering.

I'd like the class declaration of MyClass not to change too much. What's a way to keep its api stable, while still allowing changing member types of MyImpl. I guess it's not ideal to just always return std::partial_ordering.

2

There are 2 answers

3
Nicol Bolas On BEST ANSWER

But that heavily depends on the details of MyImpl.

No, it doesn't.

I mean yes, in a literal in-code sense, the details of the comparison are determined by MyImpl. But the comparison category of a type is not a private element of that type. It's a public interface, and that public interface needs to be defined.

In its entirety.

This is no different from any public member of a Pimpl'd type. If the type has a public interface and a private implementation, the two functions must be in sync. Any changes to one must be propagated to the other.

C++ Pimpl is not DRY. Indeed, it often involves a lot of redundancy. That's just the nature of the idiom.

If MyImpl contains only ints, the return type would be std::strong_ordering, but if it would contain floats too it would be std::partial_ordering.

And you have therefore changed the public interface of that class. You have changed when and how the user can compare instances of that type.

This is not a private matter, so the public interface needs to change accordingly.

0
Pepijn Kramer On

I would do it like this, with the help of an abstract baseclass (interface) for the implementation class. This will allow the outer class to use the interface to the implmentation and get information out of it.

Demo : https://onlinegdb.com/8Y0hdErUA

#include <memory>
#include <iostream>

//-----------------------------------------------------------------------------
// class_impl_itf.h
// 
// start by defining an interface for your implementation
// this allows the outer class to call functions without knowing
// its implementation yet.

class class_impl_itf
{
public:
    virtual int get_value() const noexcept = 0;
    virtual ~class_impl_itf() = default;

protected:
    class_impl_itf() = default;
};

//-----------------------------------------------------------------------------
// class.h
// #include "class_impl_itf.h"

class class_t final
{
public:
    explicit class_t(int value);
    ~class_t() = default;

    // define the compare operator
    friend inline auto operator<=>(const class_t& lhs, const class_t& rhs)
    {
        return lhs.m_pimpl->get_value() <=> rhs.m_pimpl->get_value();
    }

private:
    // do not directly refer to the implementation class but its interface
    std::unique_ptr<class_impl_itf> m_pimpl;
};

//-----------------------------------------------------------------------------
// class_impl.h
// #include "class_impl_itf.h"

class class_impl_t final : 
    public class_impl_itf
{
public:
    explicit class_impl_t(int value);
    int get_value() const noexcept override;

private:
private:
    int m_value;
};

//-----------------------------------------------------------------------------
// class.cpp
// #include "class.h"
// #include "class_impl.h"

// class_t source file
class_t::class_t(int value) :
    m_pimpl{std::make_unique<class_impl_t>(value)}
{
}

//-----------------------------------------------------------------------------
// class_impl.cpp 
//
class_impl_t::class_impl_t(int value) :
    m_value{value}
{
}

int class_impl_t::get_value() const noexcept
{
    return m_value;
}

//-----------------------------------------------------------------------------
// main.cpp 
// #include <class.h>
int main()
{
    class_t obj1{1};
    class_t obj2{2};

    if (obj1 < obj2)
    {
        std::cout << "obj1 < obj2";
    }

    return 0;
}