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();
}
};
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:
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 to 1, not 2. 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!
Why can’t I specialize std::hash inside my own namespace?
https://ift.tt/CydOFjf
Arthur O’Dwyer
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 astd::unordered_set
. Then I need to write astd::hash
specialization for it. So I write:But that’s a lot of extra
my::
-qualification, and (even worse) requires that I remember from lineA
all the way down to lineD
that I need to specializehash
for my type. I’d vastly prefer to provide the implementation ofhash<Book>
right next toBook
itself, like this:Sadly, C++ doesn’t let us do this. There have been at least two WG21 proposals to allow this, but both were abandoned:
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.Nor do we want to permit:
So, one might ask, why should it work any differently when
A
andB
are namespaces rather than classes?On the other hand, clearly it does work differently for namespaces versus classes. Consider these two perfectly parallel snippets:
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:
The problem is name lookup
The real problem is name lookup. Consider (Godbolt):
The call on line
C
findsA::f
, not::f
, because there, although that line is lexically inside the global namespace, it is also inside a specialization ofA::Hash
and thus logically inside namespaceA
. The same happens if we replace the class templates with function templates (Godbolt).Now, what happens if we make this code legal:
Does line
C
callA::f
(because we’re logically inside a specialization ofA::Hash
), orB::f
(because we’re lexically insidenamespace B
)? Obviously, by the above logic, it must callA::f
. But consider how awkward this would be in practice:On line
D
we can sayBook
; but on lineE
we must saymy::Book
, because at that point we’re logically inside namespacestd
and a lookup forBook
inside namespacestd
wouldn’t find anything. Worse, if its fully qualified name is something likemycompany::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 instd
, this gotcha might really hurt. For example:By the name-lookup logic above, line
D
meansmy::vector<T>
but lineE
meansstd::vector<T>
. This could lead to very confusing error messages later in the program — or worse, runtime misbehavior, ifmy::vector
is implicitly convertible tostd::vector
!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()
toBook::hash()
, so that its code appears in the correct logical scope (my
rather thanstd
). Could we just delegate a little more?Now we can use the unqualified name
Book
on both linesD
andE
. But it turns out that the base-clause on lineF
is also logically within namespacestd
, not namespacemy
([basic.scope.class]/1), so on lineF
we still need to spell outmy::Book
with full qualification. Factoring outBook::Hash
hasn’t gained us much!Conclusion
I’d like to see C++ gain the ability to define specializations of
std::hash
lexically withinnamespace 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!
via Arthur O’Dwyer
September 9, 2024 at 10:13AM