3

Currently, I'm storing a collection of std::unique_ptrs to heap allocated objects of a polymorphic type:

struct Foo {
  virtual ~Foo() = default;
};
Collection<std::unique_ptr<Foo>> foos;

The basic interface I need is putting / taking owners of Foo to / from foos. The objects stored in foos are never supposed to be nullptr so I'd like to replace runtime assert(owner_taken) with compile-time checks. Moreover, I would like to be capable of using non-null owners in the context where a nullable one may be expected.

Probably, I need to store something like unique_ref but how would I extract one from foos? I don't want a copy, I want the stored object itself, so owner->clone() isn't a solution. Neither I can std::move(owner) because the state of a "unique reference" would be invalid afterwards.

Is there a clean design decision?

trincot
  • 317,000
  • 35
  • 244
  • 286
passing_through
  • 1,778
  • 12
  • 24
  • 2
    If you don't allow a non-owning state in the never-null unique owner, then you can never move from it, because there wouldn't be any valid state for that. So such a type would always be non-movable. Is that really what you want? Would the collection work with non-movable types? – walnut Feb 08 '20 at 16:35
  • 1
    As a compromise, you could make your own nullptr-throwing unique_ptr that will throw when it holds a nullptr and an attempt to access the nullptr it is made or throw if attempted construction with a nullptr. (For example, after having its innards moved out from within, leaving behind an empty husk.) – Eljay Feb 08 '20 at 16:36
  • @walnut it's still nice if I'm suggested anything else which still provides compile-time guarantees of non-nullability in cases when one is needed. – passing_through Feb 08 '20 at 16:37
  • 1
    But can your collection handle immovable types? E.g. `std::vector` can't. – HolyBlackCat Feb 08 '20 at 16:38
  • @Eljay I'm afraid it's too close to `assert(owner)` which I want to avoid. – passing_through Feb 08 '20 at 16:38
  • @HolyBlackCat I'm usually using `std` ones but I'm open for more suitable alternatives. As I know, `map` allows to `extract()` even immovable types, doesn't it? – passing_through Feb 08 '20 at 16:39
  • Yes, or you can use `std::list` if you don't need a map. – HolyBlackCat Feb 08 '20 at 16:41
  • @HolyBlackCat it's inspiring yet I still have to find a suitable `Owner` :) – passing_through Feb 08 '20 at 16:45
  • If you can consider alternative programming languages, Rust supports the idiom you are looking for. – Eljay Feb 08 '20 at 16:59
  • @Eljay it would be too radical I'm afraid :D How does Rust allow this feature? I don't think it is impossible to emulate it using C++. – passing_through Feb 08 '20 at 17:03
  • Rust has the feature of a strong ownership model. Attempting to access a variable that has been moved from is a compile-time error. I don't know how you would emulate it using C++, as it is a language feature of Rust that is checked at compile-time. – Eljay Feb 08 '20 at 19:12
  • Part of the strong ownership model in Rust is a feature called "[type state](https://stackoverflow.com/questions/3210025/what-is-typestate)". – Eljay Feb 08 '20 at 20:20
  • `extract()` from `std::map` returns a [node handle](https://en.cppreference.com/w/cpp/container/node_handle) which can also be empty and accessing an empty node handle is _undefined behavior_. So it has very similar semantics as a `std::unique_ptr`. – darkdragon Sep 17 '22 at 14:59

2 Answers2

2

Is there a never-null unique owner of heap allocated objects?

There is no such type in the standard library.

It is possible to implement such type. Simply define a type with unique_ptr member and mark the default constructor deleted. You can mark constructor from std::nullptr_t deleted also so that construction from nullptr will be prevented at compile time.

Whether you construct from an external pointer, or allocate in the constructor, there is no alternative for checking for null at runtime.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • What about references? `T&` is guaranteed to never be null. – passing_through Feb 08 '20 at 18:23
  • @passing_through Not in legal code, but it's unfortunately easy to write [illegal code](https://ideone.com/1bYOnq). – user4581301 Feb 08 '20 at 18:26
  • @user4581301 actually, examples like the one you've provided are why I'm here trying to use references rather than pointers: I can be sure they aren't null. If my question has an answer with all the expected features, I can limit using pointers to only the cases when a value can actually be null so it's not a pain to always check that special case. – passing_through Feb 08 '20 at 18:34
  • @passing_through I would strongly recommend against taking ownership of addresses of objects through references. Too easy to accidentally pass non-owned objects such as automatic ones. – eerorika Feb 08 '20 at 18:40
  • @eerorika you're right: just because of this danger I want a special pseudo-reference owner class (instead of just `&`) for owning objects on the heap. – passing_through Feb 08 '20 at 18:47
-1

Reading your question, I interpret the following requirements:

  1. You don't want to copy or move the object itself (Foo)
  2. You don't want a wrapper which has some sort of empty state which excludes move semantics
  3. The object itself (Foo) should be stored on the heap such that its lifetime is independent of the control flow
  4. The object itself (Foo) should be deleted once it is not used any more

As construction / destruction, copy and move are the only ways to get objects into / out of a container, the only thing left is a wrapper object which is copied when moved into / out of the container.

You can create such an object yourself as follows:

// LICENSE: MIT

#include <memory>
#include <utility>

template<typename T>
class shared_ref {
public:
    template<typename ...Args>
    shared_ref(Args&&... args)
        : data{new T(std::forward<Args>(args)...)} 
    {}
    shared_ref(shared_ref&&) = delete;
    shared_ref& operator=(shared_ref&&) = delete;

    T& get() noexcept {
        return *data;
    }
    const T& get() const noexcept {
        return *data;
    }

    operator T&() noexcept {
        return get();
    }
    operator const T&() const noexcept {
        return get();
    }

    void swap(shared_ref& other) noexcept {
        return data.swap(other);
    }

private:
    std::shared_ptr<T> data;
};

template<class T>
void swap(shared_ref<T>& lhs, shared_ref<T>& rhs) noexcept {
    return lhs.swap(rhs);
}

I leave it as an exercise to the reader to add support for Allocator, Deleter, operator[], implicit conversion contructor to base classes.

This can then be used as follows:

#include <iostream>

int main() {
    shared_ref<int> r;  // default initialized
    std::cout << "r=" << r << std::endl;
    r = 5;  // type conversion operator to reference
    std::cout << "r=" << r << std::endl;

    shared_ref<int> s{7};  // initialized with forwarded arguments
    std::cout << "s=" << s << std::endl;

    std::swap(r, s);
    std::cout << "r=" << r << ", " << "s=" << s << std::endl;

    s = r;  // copy assignment operator
    std::cout << "s=" << s << std::endl;

    const shared_ref<int> t{s}; // copy constructor
    std::cout << "t=" << t << std::endl;
    //t = 8;  // const ref from a const object is immutable
    
    return 0;
}
darkdragon
  • 392
  • 5
  • 13