Skip to content

Commit 2faef85

Browse files
authored
Merge pull request #227 from OpenMS/feature/std-hash-support
Add support for std::hash in wrap-hash directive
2 parents 18a9698 + 7be068d commit 2faef85

File tree

5 files changed

+446
-10
lines changed

5 files changed

+446
-10
lines changed

autowrap/CodeGenerator.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -671,18 +671,33 @@ def create_wrapper_for_class(self, r_class: ResolvedClass, out_codes: CodeDict)
671671
)
672672

673673
if len(r_class.wrap_hash) != 0:
674-
class_code.add(
675-
"""
674+
hash_expr = r_class.wrap_hash[0].strip()
675+
# If hash expression is "std" or empty, use std::hash<T>
676+
if hash_expr.lower() == "std" or not hash_expr:
677+
class_code.add(
678+
"""
679+
|
680+
| def __hash__(self):
681+
| # Uses C++ std::hash<$cy_type> specialization
682+
| cdef cpp_hash[$cy_type] hasher
683+
| return hasher(deref(self.inst.get()))
684+
|
685+
""",
686+
locals(),
687+
)
688+
else:
689+
class_code.add(
690+
"""
676691
|
677692
| def __hash__(self):
678693
| # The only required property is that objects which compare equal have
679694
| # the same hash value:
680695
| return hash(deref(self.inst.get()).%s )
681696
|
682697
"""
683-
% r_class.wrap_hash[0],
684-
locals(),
685-
)
698+
% hash_expr,
699+
locals(),
700+
)
686701

687702
if len(r_class.wrap_len) != 0:
688703
class_code.add(
@@ -2089,6 +2104,15 @@ def create_default_cimports(self):
20892104
|from libcpp.memory cimport shared_ptr
20902105
"""
20912106
)
2107+
# Add std::hash declaration for wrap-hash support (named cpp_hash to avoid conflict with Python hash)
2108+
code.add(
2109+
"""
2110+
|cdef extern from "<functional>" namespace "std" nogil:
2111+
| cdef cppclass cpp_hash "std::hash" [T]:
2112+
| cpp_hash() except +
2113+
| size_t operator()(const T&) except +
2114+
"""
2115+
)
20922116
if self.include_numpy:
20932117
code.add(
20942118
"""

docs/README.md

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,15 @@ directives are:
177177
`__dealloc__` and inst attribute (but their presence is still expected).
178178
This is useful if you cannot use the shared-ptr approach to store a reference
179179
to the C++ class (as with singletons for example).
180-
- `wrap-hash`: If the produced class should be hashable, give a hint which
181-
method should be used for this. This method will be called on the C++ object
182-
and fed into the Python "hash" function. This implies the class also provides
183-
a `operator==` function. Note that the only requirement for a hash function is
184-
that equal objects produce equal values.
180+
- `wrap-hash`: If the produced class should be hashable, specify how to compute
181+
the hash. This implies the class also provides an `operator==` function. Note
182+
that the only requirement for a hash function is that equal objects produce
183+
equal values. There are two modes:
184+
- **Expression-based**: Provide a C++ expression (e.g., `getValue()`) that will
185+
be called on the C++ object and fed into Python's `hash()` function.
186+
- **std::hash-based**: Use `std` to leverage the C++ `std::hash<T>` template
187+
specialization for the class. This requires that `std::hash<YourClass>` is
188+
specialized in your C++ code.
185189
- `wrap-with-no-gil`: Autowrap will release the GIL (Global interpreter lock)
186190
before calling this method, so that it does not block other Python threads.
187191
It is advised to release the GIL for long running, expensive calls into
@@ -245,6 +249,37 @@ Additionally, TemplatedClass[U,V] gets additional methods from C[U] and from D w
245249

246250
Finally, the object is hashable in Python (assuming it has a function `getName()` that returns a string).
247251

252+
#### wrap-hash Examples
253+
254+
**Expression-based hash** (calls a method and passes result to Python's `hash()`):
255+
256+
```
257+
cdef cppclass MyClass:
258+
# wrap-hash:
259+
# getValue()
260+
```
261+
262+
**std::hash-based hash** (uses C++ `std::hash<MyClass>` specialization):
263+
264+
```
265+
cdef cppclass MyClass:
266+
# wrap-hash:
267+
# std
268+
```
269+
270+
For the `std` mode to work, you need to provide a `std::hash` specialization in your C++ code:
271+
272+
```cpp
273+
namespace std {
274+
template <>
275+
struct hash<MyClass> {
276+
size_t operator()(const MyClass& obj) const noexcept {
277+
return std::hash<int>{}(obj.getValue());
278+
}
279+
};
280+
}
281+
```
282+
248283
### Docstrings
249284
250285
Docstrings can be added to classes and methods using the `wrap-doc` statement. Multi line docs are supported with

tests/test_files/hash_test.hpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#ifndef HASH_TEST_HPP
2+
#define HASH_TEST_HPP
3+
4+
#include <string>
5+
#include <functional>
6+
7+
// Class that uses expression-based hash (old behavior)
8+
class ExprHashClass {
9+
private:
10+
int value_;
11+
std::string name_;
12+
13+
public:
14+
ExprHashClass() : value_(0), name_("default") {}
15+
ExprHashClass(int value, const std::string& name) : value_(value), name_(name) {}
16+
ExprHashClass(const ExprHashClass& other) : value_(other.value_), name_(other.name_) {}
17+
18+
int getValue() const { return value_; }
19+
std::string getName() const { return name_; }
20+
21+
bool operator==(const ExprHashClass& other) const {
22+
return value_ == other.value_ && name_ == other.name_;
23+
}
24+
bool operator!=(const ExprHashClass& other) const {
25+
return !(*this == other);
26+
}
27+
};
28+
29+
// Class that uses std::hash (new behavior)
30+
class StdHashClass {
31+
private:
32+
int id_;
33+
std::string label_;
34+
35+
public:
36+
StdHashClass() : id_(0), label_("") {}
37+
StdHashClass(int id, const std::string& label) : id_(id), label_(label) {}
38+
StdHashClass(const StdHashClass& other) : id_(other.id_), label_(other.label_) {}
39+
40+
int getId() const { return id_; }
41+
std::string getLabel() const { return label_; }
42+
43+
bool operator==(const StdHashClass& other) const {
44+
return id_ == other.id_ && label_ == other.label_;
45+
}
46+
bool operator!=(const StdHashClass& other) const {
47+
return !(*this == other);
48+
}
49+
};
50+
51+
// std::hash specialization for StdHashClass
52+
namespace std {
53+
template <>
54+
struct hash<StdHashClass> {
55+
size_t operator()(const StdHashClass& obj) const noexcept {
56+
// Combine id and label hashes
57+
size_t h1 = std::hash<int>{}(obj.getId());
58+
size_t h2 = std::hash<std::string>{}(obj.getLabel());
59+
return h1 ^ (h2 << 1);
60+
}
61+
};
62+
}
63+
64+
// Another class with std::hash to test template instantiations
65+
template <typename T>
66+
class TemplatedHashClass {
67+
private:
68+
T data_;
69+
70+
public:
71+
TemplatedHashClass() : data_() {}
72+
TemplatedHashClass(const T& data) : data_(data) {}
73+
TemplatedHashClass(const TemplatedHashClass& other) : data_(other.data_) {}
74+
75+
T getData() const { return data_; }
76+
77+
bool operator==(const TemplatedHashClass<T>& other) const {
78+
return data_ == other.data_;
79+
}
80+
bool operator!=(const TemplatedHashClass<T>& other) const {
81+
return !(*this == other);
82+
}
83+
};
84+
85+
// std::hash specialization for TemplatedHashClass<int>
86+
namespace std {
87+
template <>
88+
struct hash<TemplatedHashClass<int>> {
89+
size_t operator()(const TemplatedHashClass<int>& obj) const noexcept {
90+
return std::hash<int>{}(obj.getData());
91+
}
92+
};
93+
}
94+
95+
#endif // HASH_TEST_HPP

tests/test_files/hash_test.pxd

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# cython: language_level=3
2+
from libcpp.string cimport string as libcpp_string
3+
from libcpp cimport bool
4+
5+
cdef extern from "hash_test.hpp":
6+
7+
# Test case 1: Expression-based hash (old behavior, regression test)
8+
cdef cppclass ExprHashClass:
9+
# wrap-hash:
10+
# getValue()
11+
#
12+
ExprHashClass()
13+
ExprHashClass(int value, libcpp_string name)
14+
ExprHashClass(ExprHashClass) # wrap-ignore
15+
16+
int getValue()
17+
libcpp_string getName()
18+
19+
bool operator==(ExprHashClass)
20+
bool operator!=(ExprHashClass)
21+
22+
23+
# Test case 2: std::hash-based hash (new behavior)
24+
cdef cppclass StdHashClass:
25+
# wrap-hash:
26+
# std
27+
#
28+
StdHashClass()
29+
StdHashClass(int id, libcpp_string label)
30+
StdHashClass(StdHashClass) # wrap-ignore
31+
32+
int getId()
33+
libcpp_string getLabel()
34+
35+
bool operator==(StdHashClass)
36+
bool operator!=(StdHashClass)
37+
38+
39+
# Test case 3: Template class with std::hash
40+
cdef cppclass TemplatedHashClass[T]:
41+
# wrap-hash:
42+
# std
43+
#
44+
# wrap-instances:
45+
# TemplatedHashInt := TemplatedHashClass[int]
46+
#
47+
TemplatedHashClass()
48+
TemplatedHashClass(T data)
49+
TemplatedHashClass(TemplatedHashClass[T]) # wrap-ignore
50+
51+
T getData()
52+
53+
bool operator==(TemplatedHashClass[T])
54+
bool operator!=(TemplatedHashClass[T])

0 commit comments

Comments
 (0)