Why can’t I specialize std::hash
inside my own namespace?
std::hash
inside my own namespace?This question comes up a lot on the cpplang Slack.
Suppose I have a class named my::Book
, and I want to put it into a std::unordered_set
.
Then I need to write a std::hash
specialization for it. So I write:
namespace my {
struct Book { ~~~~ }; // A
struct Library { ~~~~ };
} // namespace my
template<>
struct std::hash<my::Book> { // D
size_t operator()(const my::Book& b) const {
return b.hash();
}
};
See “Don’t reopen
namespace std
” (2021-10-27).
But that’s a lot of extra my::
-qualification, and (even worse) requires that I remember from line A
all the
way down to line D
that I need to specialize hash
for my type. I’d vastly prefer to provide the
implementation of hash<Book>
right next to Book
itself, like this:
namespace my {
struct Book { ~~~~ }; // A
template<>
struct std::hash<Book> { // D
size_t operator()(const Book& b) const {
return b.hash();
}
};
struct Library { ~~~~ };
} // namespace my
Sadly, C++ doesn’t let us do this. There have been at least two WG21 proposals to allow this, but both were abandoned:
- N3730 and N3867 “Specializations and namespaces” (Mike Spertus, 2014)
- P0665 “Allow Class Template Specializations in Associated Namespaces” (Tristan Brindle, 2018)
See also CWG374 “Can explicit specialization outside namespace use qualified name?”, N3064 “Explicit specialization outside a template’s parent,” CWG1077 “Explicit specializations in non-containing namespaces,” and EWG48 “Specializations and namespaces.”
Analogous case for member templates
The status quo is that std::hash
can be specialized only “in any scope in which the corresponding primary template may be defined”
([temp.expl.spec]/3, [temp.spec.partial.general]/6).
This wording originated in response to CWG727 “In-class explicit specializations” (2008) —
see also CWG1755 “Out-of-class partial specializations of member templates” and
N4090 — where the problem they were all thinking about was
the problem of member templates. We certainly don’t want to permit e.g.
struct A {
template<class> struct Hash;
};
struct B {
template<> struct A::Hash<int> {}; // error
};
Nor do we want to permit:
struct A {
template<class> struct Hash;
struct B {
template<> struct Hash<int> {}; // error
};
};
Incidentally, GCC doesn’t support explicit specializations in member scope at all; that’s GCC bug #85282.
So, one might ask, why should it work any differently when A
and B
are namespaces rather than classes?
On the other hand, clearly it does work differently for namespaces versus classes. Consider these two perfectly parallel snippets:
struct A {
struct B {
template<class> struct Hash;
};
template<> struct B::Hash<int> {}; // error
};
namespace A {
namespace B {
template<class> struct Hash;
}
template<> struct B::Hash<int> {}; // OK
}
The former is ill-formed (which seems to me like an excellent idea).
The latter is OK; in fact, it’s exactly how we specialize std::hash
today.
So there’s nothing terribly inconsistent with our wanting to treat these two snippets differently also:
struct A {
template<class> struct Hash;
};
struct B {
template<> struct A::Hash<int> {}; // error; we'd like to keep it an error
};
namespace A {
template<class> struct Hash;
}
namespace B {
template<> struct A::Hash<int> {}; // error, but we'd like to make it OK
}
The problem is name lookup
The real problem is name lookup. Consider (Godbolt):
namespace A {
int f() { return 1; }
template<class> struct Hash;
}
int f() { return 2; }
template<> struct A::Hash<int> {
int g() { return f(); } // C
};
The call on line C
finds A::f
, not ::f
, because there, although that line is lexically
inside the global namespace, it is also inside a specialization of A::Hash
and thus logically
inside namespace A
. The same happens if we replace the class templates with function templates
(Godbolt).
Now, what happens if we make this code legal:
namespace A {
int f() { return 1; }
template<class> struct Hash;
}
namespace B {
int f() { return 2; }
template<> struct A::Hash<int> {
int g() { return f(); } // C
};
}
Does line C
call A::f
(because we’re logically inside a specialization of A::Hash
),
or B::f
(because we’re lexically inside namespace B
)? Obviously, by the above logic, it must
call A::f
. But consider how awkward this would be in practice:
namespace my {
struct Book { ~~~~ };
template<>
struct std::hash<Book> { // D
size_t operator()(const my::Book& b) const { // E
~~~~
}
};
}
On line D
we can say Book
; but on line E
we must say my::Book
, because at that point we’re
logically inside namespace std
and a lookup for Book
inside namespace std
wouldn’t find anything.
Worse, if its fully qualified name is something like mycompany::my::detail::Book
, we’ll have to
spell out that whole thing!
A potentially dangerous pitfall
If your type in namespace my
shares its name with something in std
, this gotcha might
really hurt. For example:
namespace my {
template<class T>
struct vector { ~~~~ };
template<class T>
struct std::hash<vector<T>> { // D
size_t operator()(const vector<T>& v) const; // E
};
}
By the name-lookup logic above, line D
means my::vector<T>
but line E
means std::vector<T>
.
This could lead to very confusing error messages later in the program —
or worse, runtime misbehavior, if my::vector
is implicitly convertible to std::vector
!
Note a similar pitfall with variable initializers (Godbolt):
namespace A { extern int g; } namespace A { int f() { return 1; } } int f() { return 2; } int A::g = f();
initializes
A::g
to1
, not2
. I think any working programmer would be shocked by that result; but it never comes up in practice.
An almost-workaround
Here’s almost (but not quite) a clever workaround for the above deficiency.
We’re already delegating the body of std::hash<Book>::operator()
to Book::hash()
, so that
its code appears in the correct logical scope (my
rather than std
). Could we
just delegate a little more?
namespace my {
struct Book {
struct Hash {
size_t operator()(const Book& b) const { ~~~~ } // E
};
};
template<> struct std::hash<Book> // D
: my::Book::Hash {}; // F
}
Now we can use the unqualified name Book
on both lines D
and E
. But it turns out
that the base-clause on line F
is also logically within namespace std
,
not namespace my
([basic.scope.class]/1),
so on line F
we still need to spell out my::Book
with full qualification.
Factoring out Book::Hash
hasn’t gained us much!
Conclusion
I’d like to see C++ gain the ability to define specializations of std::hash
lexically
within namespace my
. Despite the downside of name lookup’s requiring fully qualified names
(and the pitfall above when you forget full qualification in a critical place),
the ergonomic benefits would still be enormous.
Still, it would certainly be much easier to sell the feature if it didn’t have that pitfall. Do you have an idea that would solve the name-lookup pitfall — without breaking any of the other examples in this post? If you do, please, contact me via the email link below!