6
\$\begingroup\$

I'm trying to ramp up on c++17, and this class fell out in response to a bug I had. It's self-contained enough that it should be reviewable.

What I'm looking for is c++17-ness: there's so much bad or outdated info floating around that I'm leery of almost everything.

The registry will hold whatever you give it by some kind of reference: raw pointer, smart pointer, ref, etc. It will give it back in various formats compatible with the ownership rules you established: if you enroll a smart pointer, you can ask for a raw pointer back; if you enroll a unique pointer, you cannot ask for a shared pointer back. When you're done, you can withdraw enrollment and the item will be discarded or destroyed. You can ask for a unique pointer back as part of withdrawing, though.

There is a scoped enrollment option, which is what drove the whole thing. If you scoped-enroll (ref only) you get a poltergeist that will withdraw the enrollment when it dies.

A stand-alone version of the code is here, with dependencies and build commands: https://bitbucket.org/aghast/registry

The code uses doctest for testing, with some proxy macros in the doctest_proxy.hpp file for shorter, lower-case names.

// lib-agb/registry.hpp         # vim:set noet sts=8 sw=8 ts=8 tw=80:
#pragma once
/*****

NOTE: For documentation on this file, run "perldoc <file>".

=head1 NAME

Registry<V, K> - a registry for storing/retrieving objects

=head1 SYNOPSIS

    #include "registry.hpp"
    // defines:
    //   class Registry<V, K=std::string>;

    auto obj = returnSomeType();
    std::shared_ptr<SomeType> sp;
    std::unique_ptr<SomeType> up;
    std::weak_ptr<SomeType> wp;

    // CREATE a registry

    auto reg { Registry<SomeType>() };

    // ENROLL objects in registry with key

    reg.enroll("some name", &obj);
    reg.enroll("some name", obj);   // by reference
    reg.enroll("some name", sp);
    reg.enroll("some name", up);
    reg.enroll("some name", wp);

    // Special "scoped_enroll" for RAII

    if (auto se = reg.scoped_enroll("some name", obj)) {

        // ... stuff with obj in registry

    } // scope of se ends, obj withdrawn from reg

    // QUERY registry for key

    bool found = reg.contains("some name");

    // LOOKUP object by key

    SomeType * ptr = reg.get("some name");
    if (ptr == nullptr) {...error...}

    sp = reg.get_shared("some name");
    if (sp == nullptr) {...error...}

    wp = reg.get_weak("some name");
    if (wp.lock() == nullptr) {...error...}

    // WITHDRAW object from registry
    reg.withdraw("some name");
    up = std::move(reg.withdraw_unique("some name"));

=cut */

#include <iostream>     // std::ostream
#include <memory>       // std::shared_ptr, unique_ptr, weak_ptr
#include <string>       // std::string
#include <type_traits>      // std::add_pointer, add_lvalue_reference
#include <unordered_map>    // std::unordered_map
#include <utility>      // std::move

namespace agb
{

/*****

=head1 DESCRIPTION

A Registry is a place to store things, usually I<globally.> It may be global
between two functions, or two invocations of the same function, within a
translation unit, within a library, or within an application. A registry is an
example of the B<Registry> I<design pattern,> see
https://martinfowler.com/eaaCatalog/registry.html

This class only accepts objects of one type, the C<V> template parameter.
The key type defaults to C<std::string,> but may be replaced by any other type
that can be used with a C<std::unordered_map>. (This effectively means there
must be an I<enabled specialization> of C<std::hash> for the type.)

Containment can be tested using C<contains(key)>.

Values are inserted into the underlying map by calling C<enroll(key, value)>.
A special C<scoped_enroll> method exists to support RAII-style enrollments.
When the returned object goes out of scope, its destructor withdraws the
enrolled object.

Values can be retrieved using C<get(key)> to return a I<raw pointer>, or
C<get_shared>, or C<get_weak> to return other forms. There is
no variant that returns a C<unique_ptr>, see C<withdraw_unique>.

Finally, values can be permanently removed from the registry using
C<withdraw(key)>. Unique pointers can optionally be withdrawn using
C<withdraw_unique> to preserve the pointer. (Calling C<withdraw> on a key
whose item is stored with a unique pointer will destroy the object.)

=head2 Ownership Semantics

Registry objects support owning and non-owning semantics. If a raw pointer
or reference is passed, the registry does not own the object. If a shared
pointer or weak pointer is passed, the registry object shares ownership
until C<withdraw...> is called. If a C<unique_ptr> is passed, the registry
object takes ownership until C<withdraw...> is called.

=head2 Types

Template class C<Registry<V, KE<gt>> (hereafter C<Registry>) defines the
following public types:

=over 4

=item C<Registry::key_t = K>

Type of the key to use for enrollment and lookup. The template parameter has
a default of C<std::string>.

=item C<Registry::value_t = V>

Type of the stored value. Used to construct pointer/reference typenames.

=item C<Registry::raw_ptr_t>

Type of a raw pointer: C<V *>

=item C<Registry::ref_t>

Type of a reference: C<V &>

=item C<Registry::shared_ptr_t>

Type of a shared pointer: C<std::shared_ptr<VE<gt>>

=item C<Registry::unique_ptr_t>

Type of a unique pointer: C<std::unique_ptr<VE<gt>>

=item C<Registry::weak_ptr_t>

Type of a weak pointer: C<std::weak_ptr<VE<gt>>

=back

=cut */

// INTERNAL DETAILS. Nothing to see here, move along!
namespace detail
{
template <typename T>
struct traits
{
    using value_t = typename std::remove_pointer<
        typename std::remove_reference<T>::type>::type;
    using ref_t   = typename std::add_lvalue_reference<value_t>::type;

    using raw_ptr_t    = typename std::add_pointer<value_t>::type;
    using shared_ptr_t = typename std::shared_ptr<value_t>;
    using unique_ptr_t = typename std::unique_ptr<value_t>;
    using weak_ptr_t   = typename std::weak_ptr<value_t>;
};


template <typename V>
struct entry_t
{
    using value_t = typename traits<V>::value_t;
    using ref_t   = typename traits<V>::ref_t;

    using raw_ptr_t    = typename traits<V>::raw_ptr_t;
    using shared_ptr_t = typename traits<V>::shared_ptr_t;
    using unique_ptr_t = typename traits<V>::unique_ptr_t;
    using weak_ptr_t   = typename traits<V>::weak_ptr_t;

    enum { INVALID=0, RAW, SHARED, UNIQUE, WEAK }
            tag_m {INVALID};

    union {
        raw_ptr_t   raw_m;
        shared_ptr_t    shared_m {nullptr};
        weak_ptr_t  weak_m;
        unique_ptr_t    unique_m;
    };

    // Default ctor: Explicitly defined due to union with non-trivial
    // members. See http://en.cppreference.com/w/cpp/language/union

    entry_t() {}

    // No copy constructor due to unique_ptr.

    entry_t(const entry_t & rhs) =delete;

    // Move constructor

    entry_t(entry_t && rhs)
        : tag_m {INVALID}
        , raw_m {nullptr}
    {
        using std::move;
        using std::swap;

        swap(tag_m, rhs.tag_m);

        switch (tag_m)
        {
        case INVALID:   [[fallthrough]];
        case RAW:
            swap(raw_m, rhs.raw_m);
            break;

        case SHARED:
            new(&shared_m) shared_ptr_t(move(rhs.shared_m));
            break;

        case UNIQUE:
            new(&unique_m) unique_ptr_t(move(rhs.unique_m));
            break;

        case WEAK:
            new(&weak_m) weak_ptr_t(move(rhs.weak_m));
            break;
        }
    }

    // Initializing constructors

    entry_t(raw_ptr_t item) :tag_m(RAW), raw_m(item) {}
    entry_t(ref_t item) :tag_m(RAW), raw_m(&item) {}
    entry_t(shared_ptr_t item) :tag_m(SHARED), shared_m(item) {}
    entry_t(unique_ptr_t && item)
        : tag_m(UNIQUE)
        , unique_m(std::move(item)) {}
    entry_t(weak_ptr_t item) :tag_m(WEAK), weak_m(item) {}

    // Destructor: required due to union.

    ~entry_t() { reset(); }

    // Helper. Called from dtor and operator=.
    void
    reset()
    {
        switch (tag_m)
        {
        case INVALID:   [[fallthrough]];
        case RAW:   raw_m = nullptr; break;
        case SHARED:    shared_m.reset(); break;
        case UNIQUE:    unique_m.reset(); break;
        case WEAK:  weak_m.reset(); break;
        }

        tag_m = INVALID;
    }

    // No copy assignment due to unique_ptr.

    entry_t &
    operator = (const entry_t &) =delete;

    // Move assignment

    entry_t &
    operator = (entry_t && rhs)
    {
        // First, destroy anything currently stored.
        reset();

        using std::swap;
        swap(tag_m, rhs.tag_m);

        using std::move;
        switch (tag_m)
        {
        case INVALID:   [[fallthrough]];
        case RAW:
            swap(raw_m, rhs.raw_m);
            break;
        case SHARED:
            new(&shared_m) shared_ptr_t(move(rhs.shared_m));
            break;
        case UNIQUE:
            new(&unique_m) unique_ptr_t(move(rhs.unique_m));
            break;
        case WEAK:
            new(&weak_m) weak_ptr_t(move(rhs.weak_m));
            break;
        }

        return *this;
    }

    friend std::ostream &
    operator << (std::ostream & o, const entry_t & e)
    {
        using E = entry_t<V>;

        o << "entry_t { ";

        switch (e.tag_m)
        {
        case E::INVALID: o << "INVALID"; break;
        case E::RAW:    o << "RAW: " << e.raw_m; break;
        case E::SHARED: o << "SHARED: " << e.shared_m.get(); break;
        case E::UNIQUE: o << "UNIQUE: " << e.unique_m.get(); break;
        case E::WEAK:   o << "WEAK: " << e.weak_m.lock().get(); break;
        }

        o << " } @ " << &e;
        o.flush();
        return o;
    }

}; // struct entry_t

testcase("entry_t: move assignment") {
    using entry_t = entry_t<bool>;

    // Create e1
    entry_t e1;
    e1.tag_m = entry_t::RAW;
    e1.raw_m = (bool *)&e1;

    check_eq(e1.tag_m, entry_t::RAW);
    check_eq(e1.raw_m, (bool*)&e1);

    // Move e1 into e2
    entry_t e2;
    e2 = std::move(e1);

    // Confirm e1 was destroyed, e2 inherits all the data
    check_eq(e1.tag_m, entry_t::INVALID);
    check_eq(e1.raw_m, nullptr);
    check_eq(e2.tag_m, entry_t::RAW);
    check_eq(e2.raw_m, (bool*)&e1);
}

testcase("entry_t: dtor") {
    auto spb = std::make_shared<bool>(true);

    check_eq(spb.use_count(), 1);

    if (*spb)
    {
        entry_t<bool> e1;
        e1.tag_m = entry_t<bool>::SHARED;
        new (&e1.shared_m) entry_t<bool>::shared_ptr_t(spb);

        check_eq(spb.use_count(), 2);
    }

    check_eq(spb.use_count(), 1);
}

template <typename R>
class ScopedEnrollment
{
public:
    using key_t = typename R::key_t;
    using ref_t = typename R::ref_t;

    ScopedEnrollment(R & reg, key_t key, ref_t value)
        : reg_m(reg)
        , key_m(key)
        , ok_m(reg.enroll(key, value))
    {}

    ~ScopedEnrollment() { reg_m.withdraw(key_m); }

    explicit
    operator bool() { return ok_m; }

private:
    R & reg_m;
    key_t   key_m;
    bool    ok_m;
};

} // namespace detail

// INTERNAL DETAILS end.

template<typename V, typename K=std::string>
class Registry
{
public:
    using key_t   = K;

    using value_t = typename detail::traits<V>::value_t;
    using ref_t   = typename detail::traits<V>::ref_t;

    using raw_ptr_t    = typename detail::traits<V>::raw_ptr_t;
    using shared_ptr_t = typename detail::traits<V>::shared_ptr_t;
    using unique_ptr_t = typename detail::traits<V>::unique_ptr_t;
    using weak_ptr_t   = typename detail::traits<V>::weak_ptr_t;

    auto    contains(key_t) -> bool;

    auto    enroll(key_t key, raw_ptr_t value) -> bool;
    auto    enroll(key_t key, ref_t value) -> bool;
    auto    enroll(key_t key, shared_ptr_t value) -> bool;
    auto    enroll(key_t key, weak_ptr_t value) -> bool;
    auto    enroll(key_t key, unique_ptr_t && value) -> bool;

    auto    get(key_t key) -> raw_ptr_t;
    auto    get_shared(key_t key) -> shared_ptr_t;
    auto    get_weak(key_t key) -> weak_ptr_t;

    auto    scoped_enroll(key_t key, ref_t value)
            -> detail::ScopedEnrollment<Registry>;

    auto    size() -> std::size_t;

    auto    withdraw(key_t key) -> void;
    auto    withdraw_unique(key_t key) -> unique_ptr_t;

private:
    using entry_t = detail::entry_t<value_t>;
    using map_t = std::unordered_map<key_t, entry_t>;

    map_t       map_m;
};

/*
=head2 Functions/Methods

=over 4

=item C<Registry()>

The default (empty) constructor. This is the usual way of creating an empty
registry. Use something like this,

    auto reg {Registry<MyClass>()};

to get the default key type of C<std::string>. Or spell out both value and
key types, like this:

    auto reg {Registry<MyValueType, MyKeyType>()};

=cut */

// No constructor: the compiler-supplied version is fine.

/*
=item C<reg.contains(key_t) -E<gt> bool>

Check if key is associated with a currently enrolled value. Returns C<true>
if an item is enrolled with the given key, C<false> otherwise.

=cut */

template <typename V, typename K>
bool Registry<V, K>::
contains(key_t key)
{
    auto found = map_m.find(key);
    return found != map_m.end();
}

/*
=item C<reg.enroll(key_t, /Z<>* see below *Z<>/) -E<gt> bool>

These overloads insert a key/value pair into the regisry. The various smart
pointers imply ownership semantics, while the raw pointer and reference
versions do not. Returns C<true> if the key was added to the registry,
C<false> if the key is already present with a different value. If the same
key/value pair is added again, C<enroll> will return true.  However, there
can't be two C<unique_ptr>s to the same value, and C<shared_ptr> and
C<weak_ptr> reference counts will not increase.

=item C<reg.enroll(key_t, value_t *) -E<gt> bool>

Raw pointer enrollment, with no ownership (not recommended). Memory must be
managed by the caller.

=cut */

template <typename V, typename K>
bool Registry<V, K>::
enroll(key_t key, raw_ptr_t value)
{
    auto [iter, ok] = map_m.try_emplace(key, value);
    auto & e {iter->second};
    return ok or (e.tag_m == entry_t::RAW and e.raw_m == value);
}

testcase("enroll raw_ptr") {
    Registry<int> reg;

    subcase("Registering returns true") {
        auto pi1 {new int(443)};
        auto result = reg.enroll("enroll raw_ptr", pi1);
        check_if(result);
        delete pi1;
    }

    subcase("Repeated registering returns true") {
        auto pi1 {new int(443)};
        auto result = reg.enroll("enroll raw_ptr", pi1);
        check_if(result);
        result = reg.enroll("enroll raw_ptr", pi1);
        check_if(result);
        delete pi1;
    }

    subcase("Repeat with different pointer fails") {
        auto pi1 {new int(443)};
        auto result = reg.enroll("enroll raw_ptr", pi1);
        check_if(result);

        auto pi2 {new int(445)};
        result = reg.enroll("enroll raw_ptr", pi2);
        check_false(result);
        delete pi2;
        delete pi1;
    }

    subcase("Different key okay") {
        auto pi1 {new int(443)};
        auto result = reg.enroll("enroll raw_ptr", pi1);
        check_if(result);
        delete pi1;

        auto pi2 {new int(445)};
        result = reg.enroll("different key", pi2);
        check_if(result);
        delete pi2;
    }
}

/*
=item C<reg.enroll(key_t, value_t &) -E<gt> bool>

Reference enrollment, with no ownership (not recommended). Memory must be
managed by the caller.  Note: for registration of "local" objects (those
having I<automatic storage duration>) see the C<Registry::scoped_enroll(key_t,
value_t)> method.

=cut */

template <typename V, typename K>
bool Registry<V, K>::
enroll(key_t key, ref_t value)
{
    auto [iter, ok] = map_m.emplace(key, &value);
    auto & e {iter->second};
    return ok or (e.tag_m == entry_t::RAW and e.raw_m == &value);
}

testcase("enroll ref") {
    Registry<int> reg;

    int i {398};
    int j {412};


    subcase("Enroll reference works") {
        auto result = reg.enroll("enroll ref", i);
        check_if(result);
    }

    subcase("Repeated enrollment returns true") {
        auto result = reg.enroll("enroll ref", i);
        check_if(result);

        result = reg.enroll("enroll ref", i);
        check_if(result);
    }

    subcase("Enrolling a different ref with same key fails") {
        auto result = reg.enroll("enroll ref", i);
        check_if(result);

        result = reg.enroll("enroll ref", j);
        check_false(result);
    }
}

/*
=item C<reg.enroll(key_t, std::shared_ptr<value_tE<gt>) -E<gt> bool>

Shared pointer enrollment, with shared ownership. Memory will be managed by
the shared pointer. The Registry will hold its shared pointer until a call to
C<withdraw> or C<withdraw_unique>, so mortality is not a concern. Note that
the destructor for C<Registry> will destroy the shared pointer if the shared
pointer has not been withdrawn.

=cut */

template <typename V, typename K>
bool Registry<V, K>::
enroll(key_t key, shared_ptr_t value)
{
    auto [iter, ok] = map_m.emplace(key, value);
    auto & e {iter->second};
    return ok or (e.tag_m == entry_t::SHARED and e.shared_m == value);
}

testcase("enroll shared_ptr") {
    Registry<int> reg;

    auto sp1 {std::make_shared<int>(454)};

    auto result = reg.enroll("en:sp sp", sp1);
    check_if(result);

    subcase("Registration increases use count") {
        auto use_count_before = sp1.use_count();
        auto res2 = reg.enroll("en:sp sp2", sp1);
        check_if(res2);
        check_eq(sp1.use_count(), use_count_before + 1);
    }

    subcase("Repeated enrollment returns true") {
        auto use_count_before = sp1.use_count();
        result = reg.enroll("en:sp sp", sp1);
        check_if(result);
        check_eq(sp1.use_count(), use_count_before);
    }

    subcase("Enrolling a different ref with same key fails") {
        auto sp2 {std::make_shared<int>(475)};
        result = reg.enroll("en:sp sp", sp2);
        check_false(result);
    }
}


/*
=item C<reg.enroll(key_t, std::unique_ptrE<lt>value_tE<gt>) -E<gt> bool>

Unique pointer enrollment, with exclusive ownership. Memory will be managed
by the unique pointer. The Registry will hold the unique pointer until a call
to C<withdraw> or C<withdraw_unique>, so mortality is not a concern. Note that
the destructor for C<Registry> will destroy the unique pointer, freeing the
associated memory, if it has not been withdrawn.

Note also, this takes the unique pointer by r-value reference because it
I<might not> take ownership (if the key is already in the Registry). Check
the result: true means the Registry takes ownership, false means the key
was in the Registry and ownership remains with the caller.

=cut */

template <typename V, typename K>
bool Registry<V, K>::
enroll(key_t key, unique_ptr_t && value)
{
    auto [iter, ok] = map_m.try_emplace(key, std::move(value));
    (void)iter; // unused
    return ok;
}

testcase("enroll unique_ptr") {
    Registry<int> reg;

    auto pi1 {new int(42)};
    std::unique_ptr<int> up1 {pi1};

    auto result = reg.enroll("unique p", std::move(up1));
    check_if(result);
    check_eq(up1.get(), nullptr);   // move resets up1
}

/*
=item C<reg.enroll(key_t, std::weak_ptr<value_tE<gt>) -E<gt> bool>

Weak pointer enrollment, with shared ownership. Memory will be managed by the
shared pointer that backs up the weak pointer passed in. The Registry will
hold the weak pointer until I<either> a call to C<get> is made wherein the
weak pointer is discovered to be expired, or a call to one of the
C<withdraw...> methods is made. Because the pointer may expire, mortality is a
concern. Any object enrolled using a weak pointer should be checked each time
for validity: the various C<get...> functions will return nullptr when
fetching an expired weak pointer.

=cut */

template <typename V, typename K>
bool Registry<V, K>::
enroll(key_t key, weak_ptr_t value)
{
    auto [iter, ok] = map_m.emplace(key, value);
    auto & e {iter->second};
    return ok or (e.tag_m == entry_t::WEAK
            and e.weak_m.lock() == value.lock());
}

testcase("enroll weak_ptr") {
    Registry<int> reg;

    auto sp1 {std::make_shared<int>(454)};
    auto wp1 {std::weak_ptr<int>(sp1)};

    subcase("Enrolling weak_ptr works") {
        auto result = reg.enroll("en:wp wp", wp1);
        check_if(result);
    }

    subcase("Repeated enrollment returns true") {
        auto result = reg.enroll("en:wp wp", wp1);
        check_if(result);
        result = reg.enroll("en:wp wp", wp1);
        check_if(result);
    }

    subcase("Enrolling a different pointer with same key fails") {
        auto result = reg.enroll("en:wp wp", wp1);
        check_if(result);
        auto sp2 {std::make_shared<int>(550)};
        auto wp2 {std::weak_ptr<int>(sp2)};
        result = reg.enroll("en:wp wp", wp2);
        check_false(result);
    }
}

/*
=item C<reg.get(key_t) -E<gt> value_t *>

Returns a raw pointer to the enrolled value. If the value was enrolled using
a shared or unique pointer, the smart pointer is dereferenced to find the raw
pointer.  If the value was enrolled using a weak pointer, C<nullptr> is
returned. If the C<key> is not enrolled, C<nullptr> is returned.

I<Note:> If an enrollment was via weak pointer, returning a raw pointer would
imply either (a) replacing the weak pointer with a shared pointer in the
registry, which violates weak pointer semantics since now the registry holds
the object strongly; or (b) locking the weak pointer, getting the raw pointer,
then unlocking the weak pointer again, resulting in a returned value that
might disappear at any moment. Yeah, no.

=cut */

template <typename V, typename K>
auto Registry<V, K>::
get(key_t key) -> raw_ptr_t
{
    auto found = map_m.find(key);

    if (found == map_m.end())
        return nullptr;

    auto & entry {found->second};

    switch (entry.tag_m) {
    case entry_t::INVALID:  return nullptr;
    case entry_t::RAW:  return entry.raw_m;
    case entry_t::SHARED:   return entry.shared_m.get();
    case entry_t::UNIQUE:   return entry.unique_m.get();
    default:        [[fallthrough]];
    case entry_t::WEAK: return nullptr;
    }
    /*NOTREACHED*/
}

testcase("get (raw)") {
    auto reg = Registry<int>();

    subcase("Works for raw pointers") {
        int i = 1;
        auto registered = reg.enroll("get (raw)", &i);
        check_if(registered);

        auto raw_ptr = reg.get("get (raw)");
        check_eq(raw_ptr, &i);
    }

    subcase("Fails for bad key") {
        auto raw_ptr = reg.get("bogus");
        check_eq(raw_ptr, nullptr);
    }

    subcase("Works for shared enroll") {
        auto sp = std::make_shared<int>(42);
        auto registered = reg.enroll("shared_ptr", sp);
        check_if(registered);

        auto raw_ptr = reg.get("shared_ptr");
        check_eq(raw_ptr, sp.get());
    }

    subcase("Works for unique enroll") {
        auto up = std::make_unique<int>(42);
        auto pi = up.get();
        auto registered = reg.enroll("unique_ptr", std::move(up));
        check_if(registered);

        auto raw_ptr = reg.get("unique_ptr");
        check_eq(raw_ptr, pi);
    }

    subcase("Throws trying to make raw_ptr from weak_ptr") {
        auto sp = std::make_shared<int>(771);
        auto wp = std::weak_ptr<int>(sp);
        auto registered = reg.enroll("weak_ptr", wp);
        check_if(registered);

        auto raw_ptr = reg.get("weak_ptr");
        check_eq(raw_ptr, nullptr);
    }
}

/*
=item C<reg.get_shared(key_t) -E<gt> std::shared_ptrE<lt>value_tE<gt>>

Returns a shared pointer to the enrolled value, I<if and only if> a shared or
weak pointer was enrolled. If the C<key> was not found, or if the enrolled
type is not compatible with returning a shared pointer, a null shared
pointer is returned.

=cut */

template <typename V, typename K>
auto Registry<V, K>::
get_shared(key_t key) -> shared_ptr_t
{
    auto result {shared_ptr_t()};

    auto found = map_m.find(key);

    if (found != map_m.end())
    {
        auto & entry {found->second};

        switch (entry.tag_m) {
        case entry_t::SHARED:   result = entry.shared_m; break;
        case entry_t::WEAK: result = entry.weak_m.lock(); break;
        default:        [[fallthrough]];
        }
    }

    return result;
}

testcase("get_shared") {
    auto reg = Registry<int>();

    subcase("Throws for raw enroll") {
        auto pi = new int(814);
        auto registered = reg.enroll("raw pointer", pi);
        check_if(registered);

        auto sp = reg.get_shared("raw pointer");
        check_eq(sp, nullptr);
    }

    subcase("Fails for bad key") {
        auto sp = reg.get_shared("bogus");
        check_false(sp);
    }

    subcase("Works for shared enroll") {
        auto sp = std::make_shared<int>(827);
        auto registered = reg.enroll("shared_ptr", sp);
        check_if(registered);

        auto spout = reg.get_shared("shared_ptr");
        check_eq(spout, sp);
    }

    subcase("Throws for unique enroll") {
        auto up = std::make_unique<int>(42);
        auto registered = reg.enroll("unique_ptr", std::move(up));
        check_if(registered);

        auto sp = reg.get_shared("unique_ptr");
        check_eq(sp, nullptr);
    }

    subcase("Works for weak enroll") {
        auto sp = std::make_shared<int>(844);
        auto wp = std::weak_ptr<int>(sp);
        auto registered = reg.enroll("weak_ptr", wp);
        check_if(registered);

        auto spout = reg.get_shared("weak_ptr");
        check_eq(spout, sp);
    }
}

/*
=item C<reg.get_weak(key_t)  -E<gt> std::weak_ptrE<lt>value_tE<gt>>

Returns a weak pointer to the enrolled value, I<if and only if> a shared or
weak pointer was enrolled. If the C<key> was not found, or if the enrolled
type is not compatible with returning a weak pointer, an empty weak pointer
is returned.

=cut */

template <typename V, typename K>
auto Registry<V, K>::
get_weak(key_t key) -> weak_ptr_t
{
    auto result {weak_ptr_t()};

    auto found = map_m.find(key);

    if (found != map_m.end())
    {
        auto & entry {found->second};

        switch (entry.tag_m) {
        case entry_t::SHARED:
            result = weak_ptr_t(entry.shared_m); break;
        case entry_t::WEAK:
            result = entry.weak_m; break;
        default:
            [[fallthrough]];
        }
    }

    return result;
}

testcase("get_weak") {
    auto reg = Registry<int>();

    subcase("Throws for raw enroll") {
        auto pi = new int(814);
        auto registered = reg.enroll("raw pointer", pi);
        check_if(registered);

        auto wp = reg.get_weak("raw pointer");
        check_if(wp.expired());
    }

    subcase("Fails for bad key") {
        auto wp = reg.get_weak("bogus");
        check_false(wp.lock());
    }

    subcase("Works for shared enroll") {
        auto sp = std::make_shared<int>(904);
        auto registered = reg.enroll("shared_ptr", sp);
        check_if(registered);

        auto wpout = reg.get_weak("shared_ptr");
        check_eq(wpout.lock(), sp);
    }

    subcase("Throws for unique enroll") {
        auto up = std::make_unique<int>(913);
        auto registered = reg.enroll("unique_ptr", std::move(up));
        check_if(registered);

        auto wp = reg.get_weak("unique_ptr");
        check_if(wp.expired());
    }

    subcase("Works for weak enroll") {
        auto sp = std::make_shared<int>(921);
        auto wp = std::weak_ptr<int>(sp);
        auto registered = reg.enroll("weak_ptr", wp);
        check_if(registered);

        auto wpout = reg.get_weak("weak_ptr");
        check_eq(wpout.lock(), wp.lock());
    }
}

/*
=item C<reg.scoped_enroll() -E<gt> ScopedEnrollment>

Enroll a key/value pair and return an object that will withdraw the enrollment
upon destruction. Implementing a form of RAII for enrollment. Storing the
result object in a local variable will cause the withdraw to occur at the
end of the variable's scope, hence the name.

I<Note:> C<scoped_enroll> only enrolls objects by reference. Smart pointers
are not accepted, since the purpose is to withdraw the enrollment when end of
scope is reached. For persistent enrollment, use C<enroll()> and manage the
enrollment yourself.

=cut */

template <typename V, typename K>
auto Registry<V, K>::
scoped_enroll(key_t key, ref_t value) -> detail::ScopedEnrollment<Registry>
{
    return detail::ScopedEnrollment<Registry>(*this, key, value);
}

testcase("scoped enrollment") {
    auto reg = Registry<int>();
    int i = 991;

    check_eq(reg.size(), 0);

    if (auto se = reg.scoped_enroll("ref i", i))
    {
        check_eq(reg.size(), 1);

        auto pi = reg.get("ref i");
        check_ne(pi, nullptr);
        check_eq(*pi, i);
    }

    // se out of scope - enrollment withdrawn
    check_eq(reg.size(), 0);
    auto pi = reg.get("ref i");
    check_eq(pi, nullptr);
}

/*
=item C<reg.size() -E<gt> size_t>

Returns the number of items contained in the registry (enrolled but not yet
withdrawn).

=cut */

template <typename V, typename K>
auto Registry<V, K>::
size() -> std::size_t
{
    return map_m.size();
}

/*
=item C<reg.withdraw(key_t) -E<gt> void>

Withdraw the item from the registry, removing the key/value pair so that no
subsequent lookups will find it. If the original enrollment used a smart
pointer, that pointer is released and the referenced object I<may> be freed.
(Note: this includes unique pointers. To transfer ownership of a unique
pointer from the registry to your code, see C<withdraw_unique>.

=cut */

template <typename V, typename K>
auto Registry<V, K>::
withdraw(key_t key) -> void
{
    auto found = map_m.find(key);

    if (found == map_m.end())
        return;

    map_m.erase(found);
}

testcase("withdraw") {
    auto reg = Registry<int>();

    subcase("raw pointer") {
        auto pi = new int(955);
        auto registered = reg.enroll("withdraw", pi);
        check_if(registered);

        reg.withdraw("withdraw");

        auto pi2 = reg.get("withdraw");
        check_eq(pi2, nullptr);
    }

    subcase("shared pointer") {
        auto sp = std::make_shared<int>(966);
        auto registered = reg.enroll("withdraw", sp);
        check_if(registered);
        check_eq(sp.use_count(), 2);

        reg.withdraw("withdraw");

        auto pi2 = reg.get("withdraw");
        check_eq(pi2, nullptr);
        check_eq(sp.use_count(), 1);
    }

    subcase("unique pointer") {
        auto up = std::make_unique<int>(974);
        auto registered = reg.enroll("withdraw", std::move(up));
        check_if(registered);
        check_eq(up.get(), nullptr);

        reg.withdraw("withdraw");
        check_eq(reg.size(), 0);
    }
}

/*
=item C<reg.withdraw_unique(key_t) -E<gt> unique_ptr_t>

Withdraw the item from the registry (see C<withdraw> above) and return a
unique pointer. If the item was not enrolled with a unique pointer, returns
an empty unique pointer, otherwise the enrolled pointer.

=back

=cut */

template <typename V, typename K>
[[nodiscard]] auto Registry<V, K>::
withdraw_unique(key_t key) -> unique_ptr_t
{
    unique_ptr_t result {nullptr};

    auto found = map_m.find(key);

    if (found != map_m.end())
    {
        auto & entry = found->second;

        if (entry.tag_m == entry_t::UNIQUE)
            result = std::move(entry.unique_m);

        map_m.erase(found);
    }

    return result;
}

testcase("withdraw_unique") {
    auto reg = Registry<int>();

    subcase("Fails for raw enroll") {
        auto pi = new int(1039);
        auto registered = reg.enroll("raw pointer", pi);
        check_if(registered);

        auto up = reg.withdraw_unique("raw pointer");
        check_eq(up, nullptr);
        check_eq(reg.size(), 0);
    }

    subcase("Fails for bad key") {
        auto up = reg.withdraw_unique("bogus");
        check_eq(up, nullptr);
        check_eq(reg.size(), 0);
    }

    subcase("Fails for shared enroll") {
        auto sp = std::make_shared<int>(1054);
        auto registered = reg.enroll("shared_ptr", sp);
        check_if(registered);

        auto up = reg.withdraw_unique("shared_ptr");
        check_eq(up, nullptr);
        check_eq(reg.size(), 0);
    }

    subcase("Works for unique enroll") {
        auto up = std::make_unique<int>(1063);
        auto registered = reg.enroll("unique_ptr", std::move(up));
        check_if(registered);

        auto up2 = reg.withdraw_unique("unique_ptr");
        check_eq(*up2, 1063);
        check_eq(reg.size(), 0);
    }

    subcase("Works for weak enroll") {
        auto sp = std::make_shared<int>(1073);
        auto wp = std::weak_ptr<int>(sp);
        auto registered = reg.enroll("weak_ptr", wp);
        check_if(registered);

        auto up = reg.withdraw_unique("weak_ptr");
        check_eq(up, nullptr);
        check_eq(reg.size(), 0);
    }
}

} // namespace agb
\$\endgroup\$
2
  • \$\begingroup\$ Could you show a typical use case? I find it hard to understand what kind of problem this is supposed to solve. \$\endgroup\$
    – mkrieger1
    Feb 4, 2018 at 11:09
  • \$\begingroup\$ My intended use is for things like logging or ui, where it is cleaner for various parts of your code to pull an item from the registry than to pass a log to each function. See here: martinfowler.com/eaaCatalog/registry.html \$\endgroup\$
    – aghast
    Feb 4, 2018 at 11:28

1 Answer 1

4
\$\begingroup\$

Code

  • const correctness! ScopedEnrollment::operator bool, Registry::contains, Registry::size and Registry::get functions (and perhaps I've missed some more) should all be const.

  • entry_t. Since it's C++17, could this class be replaced with std::variant or std::any (or use one of them internally).

  • (Opinion:) maybe use the names insert and erase (or remove, or register and unregister) instead of enroll and withdraw. It's more consistent with standard library stuff and probably more intuitive.

  • ScopedEnrollment::~ScopedEnrollment doesn't check ok_m before calling withdraw. This doesn't currently matter because...

  • withdraw doesn't care if the item isn't there. Which seems weird, since improving scoping seems like one of the core goals... I'd suggest either asserting or throwing if the withdrawn item is missing, depending on project error-handling requirements. Or the withdraw function could return a boolean like the enroll functions so the user code can handle or ignore it.

  • ScopedEnrollment could implement move semantics.

  • Registry - can / should it be copied / moved / assigned?


Design

(Opinion:)

Honestly, it all seems a bit complicated. There are two aspects to this:

  1. Object lifetimes.
  2. Looking objects up with a key.

And it seems confusing to tie them together like this. Especially creating a Registry<T> and then expecting to use a mixture of raw_ptr, shared_ptr, weak_ptr, unique_ptr and the ScopedEnrollment to manage lifetimes of this one type. I'd be surprised if this functionality is needed in the actual use case.

The simplest thing is to separate the two concerns out, which effectively moves the entry_t class out of the registry and thus simplifies the registry interface.


Object Lookup

The object lookup functionality could be provided with the following interface:

template<class V, class K = std::string>
class Registry
{
public:

    using value_t = V;
    using key_t = K;

    auto insert(key_t key, value_t const& value) -> bool;
    auto insert(key_t key, value_t&& value) -> bool;
    auto remove(key_t key) -> bool;
    auto clear() -> void;

    auto contains(key_t key) const -> bool;

    auto get(key_t key) const -> value_t const&; // asserts or throws if not found
    auto get(key_t key) -> value_t&; // asserts or throws if not found

    auto find(key_t key) const -> value_t const*; // returns nullptr if not found
    auto find(key_t key) -> value_t*; // returns nullptr if not found

    auto release(key_t key) -> value_t; // erases and returns object (i.e. for unique_ptr )

    auto size() const -> std::size_t;

private:

    using map_t = std::map<std::string, value_t>;
    map_t m_map;
};

So now it doesn't have to handle any type stuff internally; only object lookup.

This works fine with e.g. Registry<int>, Registry<int*>, Registry<std::shared_ptr<int>>, Registry<std::shared_ptr<void>>, Registry<std::weak_ptr<int>>, Registry<std::unique_ptr<int>> or even Registry<std::variant<std::shared_ptr<int>, std::unique_ptr<int>>>.

If it's really necessary to store multiple types in the same registry you can transparently store a variant object. (And the user code had to be aware of all the types in the variant before anyway, and know which one it needed, in order to call the appropriate Registry functions.)


Scoping

The other issue is associating object lifetimes with being in the Registry. If at all possible, it would be better to go with one consistent approach. Which leads to the following choices, with objects stored in the registry by:

  • value: the registry owns it.
  • unique_ptr: the registry owns it.
  • shared_ptr: the registry effectively owns it (if there's no registry, we don't care).
  • weak_ptr: the registry doesn't own it. We don't have to add / remove it manually, but we'll have left-over entries in the registry.
  • raw_ptr: the registry doesn't own it. We need to add / remove the pointer manually.

Note that if the registry owns the object, but we need to manually remove it at some point to manage the lifetime, then the registry probably shouldn't own it after all! (Following this logic, the registry may just end up containing raw pointers...)

It's hard to recommend one approach over another without seeing the actual use case, but can I think of the following options:

  • A class like ScopedEnrollment, that adds / removes a raw pointer from the registry. (Not ideal, because the registration / unregistration are decoupled from the object lifetime).
  • A class like ScopedEnrollment, but it actually owns the object as well, and adds / removes a pointer to the object from the registry.
  • A unique_ptr (or shared_ptr) with a custom deleter (which does the above without much extra code, but could be considered hacky...):

    template<class V>
    using RegisteredPtr = std::unique_ptr<V, std::function<void(V*)>>;
    
    template<class V, class... Args>
    RegisteredPtr<V> MakeRegisteredPtr(Registry<V*>& registry, typename Registry<V*>::key_t const& key, Args&&... args)
    {
        auto deleter = [&, key] (V* v)
        {
            registry.remove(key);
            std::default_delete<V>()(v);
        };
    
        auto v = new V(std::forward<Args>(args)...);
    
        if (!registry.insert(key, v))
        {
            std::default_delete<V>()(v);
            return RegisteredPtr<V>();
        }
    
        return RegisteredPtr<V>(v, deleter);
    }
    
    ...
    
    {
        Registry<int*> r;
    
        {
            auto p = MakeRegisteredPtr<int>(r, k, 5);
            assert(*r.get(k) == 5);
        }
    
        assert(!r.contains(k));
    }
    
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.