5
votes

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);
}
1
Yes, this is legal, there are no restrictions on specialization of std::hash except DefaultConstructible, CopyAssignable, Swappable and Destructible (and all other requirements that has nothing to do with "explicitly prohibited"). One example of "explicitly prohibited" specialization is specializing std::numeric_limits for non-arithmetic standard types.Holt
You should have a look at Argument Dependend lookup, you don't need to add functions to the std namespace.The Techel

1 Answers

4
votes

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.