20
votes

According to cppreference.com, std::shared_ptr provides a full set of relative operators (==, !=, <, ...), but the semantics of comparison aren't specified. I assume they compare the underlying raw pointers to the referenced objects, and that std::weak_ptr and std::unique_ptr do the same.

For some purposes, I would prefer to have relative operators that order the smart pointers based on comparing the referenced objects (rather than the pointers to them). This is already something I do a lot, but with my own "dumb pointers" that behave mostly like raw pointers except for the relative operators. I'd like to do the same thing with the standard C++11 smart pointers too. So...

  1. Is it OK to inherit from the C++11 smart pointers (shared_ptr, weak_ptr and unique_ptr) and override the relative operators?

  2. Are there any sneaky issues I need to look out for? For example, are there any other methods I need to implement or use using for to ensure things work correctly?

  3. For the ultimate in laziness, is there a library template available that will do this for me automatically?

I'm hoping this is an "of course you can do that, idiot!" kind of thing, but I'm a little uncertain because there are some classes in the standard library (containers like std::map at least) that you're not supposed to inherit from.

3
In general, it's not safe to inherit from anything who's destructor is not dynamic. It can be and is done commonly, you just have to be really careful.Mooing Duck
@Mooing Duck - OK, no late binding, which could be a problem for the destructor and elsewhere - makes sense. I don't think this is an issue for me, at least at the moment, but I will need to check. Perhaps it would be better to wrap a smart pointer as a member rather than to inherit.Steve314
Er, by "dynamic", I of course mean "virtual"Mooing Duck
I think you're approaching this entirely the wrong way. If you have a pointer to something and you want to get that something, you dereference the pointer. Very basic idea applied here: want to compare to things pointed to by smart pointers? Dereference them. A smart pointer should act like a pointer. Write a comparison function like dereference_compare that does what you want, keep the concepts separate.GManNickG
Perhaps another option is that your purpose (as you put it) accepts a comparator to be used for ordering. As an example, std::set<std::shared_ptr<T>> is different from std::set<std::shared_ptr<T>, indirect_less<std::shared_ptr<T>>>> (where indirect_less is left to the imagination of the reader, but should be obvious).Luc Danton

3 Answers

9
votes

In general, it's not safe to inherit from anything who's destructor is not dynamic. It can be and is done commonly, you just have to be really careful. Instead of inheriting from the pointers, I'd just use composition, especially since the number of members is relatively small. You might be able to make a template class for this

template<class pointer_type>
class relative_ptr {
public:
    typedef typename std::pointer_traits<pointer_type>::pointer pointer;
    typedef typename std::pointer_traits<pointer_type>::element_type element_type;
    relative_ptr():ptr() {}
    template<class U>
    relative_ptr(U&& u):ptr(std::forward<U>(u)) {}
    relative_ptr(relative_ptr<pointer>&& rhs):ptr(std::move(rhs.ptr)) {}
    relative_ptr(const relative_ptr<pointer>& rhs):ptr(std::move(rhs.ptr)) {}

    void swap (relative_ptr<pointer>& rhs) {ptr.swap(rhs.ptr);}
    pointer release() {return ptr.release();}
    void reset(pointer p = pointer()) {ptr.reset(p);}
    pointer get() const {return ptr.get();}
    element_type& operator*() const {return *ptr;}
    const pointer_type& operator->() const {return ptr;}

    friend bool operator< (const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::less<element>(*lhs,*rhs);}
    friend bool operator<=(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::less_equal<element>(*lhs,*rhs);}
    friend bool operator> (const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::greater<element>(*lhs,*rhs);}
    friend bool operator>=(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::greater_equal<element>(*lhs,*rhs);}
    friend bool operator==(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return *lhs==*rhs;}
    friend bool operator!=(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return *lhs!=*rhs;}
protected:
    pointer_type ptr;
};

Obviously, the simplicity of the wrapper reduces you to the lowest common denominator for the smart pointers, but whatever. They're not exactly complicated, you could make one for each of the smart pointer classes.

I will provide a warning that I don't like the way == works, since it may return true for two pointers to different objects. But whatever. I also haven't tested the code, it might fail for certain tasks, like attempting to copy when it contains a unique_ptr.

11
votes

The first thing, as others have already pointed out is that inheritance is not the way to go. But rather than the convoluted wrapper suggested by the accepted answer, I would do something much simpler: Implement your own comparator for your own types:

namespace myns {
struct mytype {
   int value;
};
bool operator<( mytype const& lhs, mytype const& rhs ) {
   return lhs.value < rhs.value;
}
bool operator<( std::shared_ptr<mytype> const & lhs, std::shared_ptr<mytype> const & rhs )
{
   // Handle the possibility that the pointers might be NULL!!!
   // ... then ...
   return *lhs < *rhs;
}
}

The magic, which is not really magic at all is Argument Dependent Lookup (aka. Koening Lookup or ADL). When the compiler encounters a function call it will add the namespace of the arguments to lookup. If the objects are the instantiation of a template, then the compiler will also add the namespaces of the types used to instantiate the template. So in:

int main() {
   std::shared_ptr<myns::mytype> a, b;
   if ( a < b ) {                       // [1]
      std::cout << "less\n";
   } else {
      std::cout << "more\n";
   }
}

In [1], and because a and b are objects user defined types (*) ADL will kick in and it will add both std and myns to the lookup set. It will then find the standard definition of operator< for std::shared_ptr that is:

template<class T, class U>
bool std::operator<(shared_ptr<T> const& a, shared_ptr<U> const& b) noexcept;

And it will also add myns and add:

bool myns::operator<( mytype const& lhs, mytype const& rhs );

Then, after lookup finishes, overload resolution kicks in, and it will determine that myns::operator< is a better match than std::operator< for the call, as it is a perfect match and in that case non-templates take preference. It will then call your own operator< instead of the standard one.

This becomes a bit more convoluted if your type is actually a template, if it is, drop a comment and I will extend the answer.


(*) This is a slight simplification. Because operator< can be implemented both as a member function or a free function, the compiler will check inside std::shared_ptr<> for member operator< (not present in the standard) and friends. It will also look inside mytype for friend functions... and so on. But at the end it will find the right one.

4
votes

It's hazardous to inherit from any class that supports assignment and copy-construction, due to the risk of cutting a derived-class instance in half by accidentally assigning it to a base-class variable. This affects most classes, and is pretty much impossible to prevent, thus requiring vigilance on the part the class's users whenever they copy instances around.

Because of this, classes intended to function as bases usually shouldn't support copying. When copying is necessary, they should provide something like Derived* clone() const override instead.

The problem you are trying to solve is probably best addressed by leaving things as they are and providing custom comparators when working with such pointers.

std::vector<std::shared_ptr<int>> ii = …;
std::sort(begin(ii), end(ii),
          [](const std::shared_ptr<int>& a, const std::shared_ptr<int>& b) {
              return *a < *b;
          });