ginkgo icon indicating copy to clipboard operation
ginkgo copied to clipboard

Alternatives to raw pointers

Open gflegar opened this issue 6 years ago • 0 comments

@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

gflegar avatar Dec 10 '18 13:12 gflegar