ginkgo
ginkgo copied to clipboard
Alternatives to raw pointers
@upsj and I discussed a bit today about Ginkgo's lend semantics. The problem we currently have is that lend
has to be spammed everywhere. For example:
y = mtx::create(...);
x->copy_from(lend(y));
// vs
x->copy_from(give(y));
// another example:
mtx->apply(lend(x), lend(y));
The question is whether we can write this as:
y = mtx::create(...);
x->copy_from(y);
// vs
x->copy_from(give(y));
// another example:
mtx->apply(x, y);
I.e. to have lend
as the default behavior wherever possible.
Option 1: use std::unique_ptr
/ std::shared_ptr
for everything
The first idea discussed (considering only unique ownership) is to use const std::unique_ptr<T> &
instead of T *
. Theoretically, this works: we just avoid using plain pointers, and use std::unique_ptr
for everywhere. However, problems begin as soon as another ownership mode (e.g. std::shared_ptr
) is added, as the number of overloads needed for each such function becomes at most nk (where the upper limit is reached for functions that do not depend on ownership), where n is the number of ownership modes, and k is the number of parameters carrying ownership information. A way to alleviate the issue slightly would be to use template parameters, but it only works for non-virtual functions, and may introduce quite a bit of issues with reference collapsing rules if the users are not extremely careful.
Option 2: create a smart pointer type with ownership-less semantics
Something like this:
template <typename T>
class pointer {
public:
pointer(std::shared_ptr<T> &p) : ptr{p.get()} {}
pointer(std::unique_ptr<T> &p) : ptr{p.get()} {}
pointer(T *p) : ptr{p} {}
T *operator->() const { return ptr; };
T *get() const { return ptr; };
private:
T *ptr;
};
This would solve the problem of exponential number of overloads, but due to an implicit conversions from std::shared_ptr<T>
and std::unique_ptr<T>
to pointer<T>
, users may misuse the pointer:
std::unique_ptr<T> func();
pointer<T> p = func();
p->whatever(); // segfault
Making clear that pointer
should only be used as a parameter (e.g. by renaming it to pointer_param<T>
) seems to fix the issue, but the following problem still remains:
std::unique_ptr<T> func();
pointer_param<T> modify(pointer_param<T> p) {
p->whatever(); // ok
return p;
}
auto p = modify(func());
p->whatever(); // segfault
NOTE: One question presenting itself here is if a function T * f(T *p) { return p; }
can be considered safe in the first place, or is it trying to abuse the lifetime of its parameter?
Option 3: leave everything as is
Leaving us with more verbose code, but safer lifetime management.
Option 4: do not use smart pointers at all (except std::unique_ptr
when preventing object slicing)
Assume that everything is initialized in global scope, and passed around as references.
While simple things like the executor would certainly be able to use that model:
std::unique_ptr<LinOp> f(Executor &exec) {
return mtx::create(exec, ...);
}
auto exec = Exec::create();
auto m = f(*exec):
More complicated things, like the following function that creates a matrix, and wraps it into a solver would not:
std::unique_ptr<LinOp> f(Executor &exec, LinOpFactory &fact) {
return fact.generate(*mtx::create(exec, ...));
}
auto exec = Exec::create();
auto fact = Factory::build().on(*exec);
auto s = f(*exec, *fact): // error the temporary matrix object used outside its scope