Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it legal to specialize std library functions for a shared_ptr of a user defined type?

Tags:

The standard says the following about specializing templates from the standard library (via What can and can't I specialize in the std namespace? )

A program may add a template specialization for any standard library template to namespace std only if the declaration depends on a user-defined type and the specialization meets the standard library requirements for the original template and is not explicitly prohibited.

Is it legal to specialize standard library templates with a standard library class specialized with a user defined class?

For example, specializing std::hash for std::shared_ptr<MyType>?

From reading the above paragraph and linked question, it sounds like it should be, as the declaration of the specialization is dependent on MyType, however "Unless explicitly prohibited" worries me slightly.

The example below compiles and works as expected (AppleClang 7.3), but is it legal?

#include <unordered_set>
#include <memory>
#include <cassert>
#include <string>

struct MyType {
    MyType(std::string id) : id(id) {}
    std::string id;
};

namespace std {
    template<>
    struct hash<shared_ptr<MyType>> {
        size_t operator()(shared_ptr<MyType> const& mine) const {
            return hash<string>()(mine->id);
        }
    };

    template<>
    struct equal_to<shared_ptr<MyType>> {
        bool operator()(shared_ptr<MyType> const& lhs, shared_ptr<MyType> const& rhs ) const {
            return lhs->id == rhs->id;
        }
    };
}

int main() {
    std::unordered_set<std::shared_ptr<MyType>> mySet;
    auto resultA = mySet.emplace(std::make_shared<MyType>("A"));
    auto resultB = mySet.emplace(std::make_shared<MyType>("B"));
    auto resultA2 = mySet.emplace(std::make_shared<MyType>("A"));
    assert(resultA.second);
    assert(resultB.second);
    assert(!resultA2.second);
}
like image 322
RichTea Avatar asked Mar 06 '17 17:03

RichTea


People also ask

What is std:: shared_ ptr in c++?

The shared_ptr type is a smart pointer in the C++ standard library that is designed for scenarios in which more than one owner might have to manage the lifetime of the object in memory.

What is std shared pointer?

std::shared_ptr is a smart pointer that retains shared ownership of an object through a pointer. Several shared_ptr objects may own the same object.

Is shared_ptr slow?

shared_ptr use is limited because shared_ptr is slow and people learned to avoid it at all cost. But having to think about ownership influences design choices and often makes things more complex and less performant that needed (e.g. it often forces to copy objects instead to just pass a pointer).

When shared_ ptr is deleted?

The object of which a shared pointer shares ownership is deleted when there are no more shared pointers sharing ownership, e.g. typically in the destructor of some shared pointer (but also in an assignment, or upon explicit reset).


1 Answers

Yes, that is legal.

It is even questionably legal to specialize for std::shared_ptr<int> at one point; I don't know if they patched that ambiguity in the standard as a defect or not.

Note that that is a poor implemenation of a hash for global use. First, because it doesn't support null shared pointers. Second, because hashing a shared pointer as always the int value is questionable. It is even dangerous, because if a shared pointer to an int in a container has that int change, you just broke the program.

Consider making your own hasher for these kind of cases.

namespace notstd {
  template<class T, class=void>
  struct hasher_impl:std::hash<T>{};

  namespace adl_helper {
    template<class T>
    std::size_t hash( T const& t, ... ) {
      return ::notstd::hasher_impl<T>{}(t);
    }
  };
  namespace adl_helper2 {
    template<class T>
    std::size_t hash_helper(T const& t) {
      using ::notstd::adl_helper::hash;
      return hash(t);
    }
  }
  template<class T>
  std::size_t hash(T const& t) {
    return ::notstd::adl_helper2::hash_helper(t);
  }

  struct hasher {
    template<class T>
    std::size_t operator()(T const& t)const {
      return hash(t);
    }
  };

}

Now this permits 3 points of customization.

First, if you override std::size_t hash(T const&) in the namespace containing T, it picks it up.

Failing that, if you specialize notstd::hasher_impl<T, void> for your type T, it picks it up.

Third, if both of those fail, it invokes std::hash<T>, picking up any specializations.

Then you can do:

std::unordered_set<std::shared_ptr<MyType>, ::notstd::hasher> mySet;

and add:

struct MyType {
  MyType(std::string id) : id(id) {}
  std::string id;
  friend std::size_t hash( MyType const& self) {
    return ::notstd::hash(self.id);
  }
  friend std::size_t hash( std::shared_ptr<MyType> const& self) {
    if (!self) return 0;
    return ::notstd::hash(*self);
  }
};

which should give you a smart hash on on shared_ptr<MyType>.

This keeps the danger that someone changes id on a shared_ptr<MyType> which breaks every container containing the shared_ptr<MyType> in a non-local manner.

Shared state is the devil; consider writing a copy on write pointer if you are really worried about copying these things being expensive.

like image 116
Yakk - Adam Nevraumont Avatar answered Sep 23 '22 10:09

Yakk - Adam Nevraumont