chromium / subspace

A concept-centered standard library for C++20, enabling safer and more reliable products and a more modern feel for C++ code.; Also home of Subdoc the code-documentation generator.
https://suslib.cc
Apache License 2.0
88 stars 15 forks source link

sus::dyn() without an explicit template parameter? #358

Open danakj opened 1 year ago

danakj commented 1 year ago

The dyn() function requires a template parameter of the type erasure class DynC of concept C currently.

Whereas into() does not, since it constructs a marker type and then uses type deduction to determine what to construct.

We should in theory be able to do the same with Dyn and have it defer choosing the reference type until the operator is called, but it may be tricky to maintain const correctness.

Here's a test demo envionment in which this could be explored: https://godbolt.org/z/Y5nGe4E16

danakj commented 1 year ago

The code in the compiler explorer just in case:

#include <concepts>
#include <cstdint>
#include <cstdio>
#include <utility>

template <class DynC, class ConcreteT>
concept TypeErasedConceptOfConcreteType = requires {
    { DynC::template SatisfiesConcept<ConcreteT> } -> std::same_as<const bool>;
    typename DynC::template DynTyped<ConcreteT>;
    requires std::is_base_of_v<DynC,
                               typename DynC::template DynTyped<ConcreteT>>;
    requires !std::is_move_constructible_v<DynC>;
    requires !std::is_move_assignable_v<DynC>;
};

template <class T>
struct Box {
    template <class P>
    constexpr static Box from(P p) noexcept {
        // T should be a DynConcept for Concept, and P satisifies that Concept.
        static_assert(T::template SatisfiesConcept<P>);
        using DynTyped = T::template DynTyped<P, P>;
        return Box(new DynTyped(std::move(p)));
    }

    T* operator->() { return t_; }

    T* t_;
};

template <class DynT, class ConcreteT>
struct [[nodiscard]] Dyn {
    static_assert(
        std::same_as<DynT,
                     std::remove_volatile_t<std::remove_reference_t<DynT>>>,
        "DynT can be const-qualified but not a reference");
    static_assert(std::same_as<ConcreteT, std::remove_cvref_t<ConcreteT>>,
                  "ConcreteT can not be qualified or a reference");

    static_assert(DynT::template SatisfiesConcept<ConcreteT>);

    Dyn(ConcreteT& t [[clang::lifetimebound]])
        requires(!std::is_const_v<DynT>)
        : t_(t) {}
    Dyn(ConcreteT&& t [[clang::lifetimebound]])
        requires(!std::is_const_v<DynT>)
        : t_(t) {}
    Dyn(const ConcreteT& t [[clang::lifetimebound]])
        requires(std::is_const_v<DynT>)
        // This drops the const qualifier on `t` however we have a const
        // qualifier on `DynT` which prevents the `t` from being accessed in a
        // non-const way through the `operator DynT&` overloads.
        : t_(const_cast<ConcreteT&>(t)) {}

    operator const DynT&() const& { return t_; }

    operator DynT&() &
        requires(!std::is_const_v<DynT>)
    {
        return t_;
    }
    operator DynT&() &&
        requires(!std::is_const_v<DynT>)
    {
        return t_;
    }

    Dyn(Dyn&&) = delete;
    Dyn& operator=(Dyn&&) = delete;

   private:
    typename DynT::template DynTyped<ConcreteT, ConcreteT&> t_;
};

/// Some concept which requires two functions.
template <class T>
concept C = requires(const T& c, T& m) {
    { c.concept_fn() };
    { m.concept_fn_mut() };
};

template <C T, class Store>
struct DynCTyped;

/// The C concept when type-erased.
struct DynC {
    template <class T>
    static constexpr bool SatisfiesConcept = C<T>;
    template <class T, class Store>
    using DynTyped = DynCTyped<T, Store>;

    DynC() = default;
    virtual ~DynC() = default;
    DynC(DynC&&) = delete;
    DynC& operator=(DynC&&) = delete;

    // Virtual concept API.
    virtual void concept_fn() const = 0;
    virtual void concept_fn_mut() = 0;
};

/// The implementation of type-erasure for the C concept.
template <C T, class Store>
struct DynCTyped final : public DynC {
    constexpr void concept_fn() const override { return c_.concept_fn(); }
    constexpr void concept_fn_mut() override { return c_.concept_fn_mut(); }

   private:
    friend class Box<DynC>;
    friend struct Dyn<DynC, T>;
    friend struct Dyn<const DynC, T>;

    /// Private constructor with friends because the `Store` needs to be
    /// correctly given (as a value or a reference) for memory safety.
    constexpr DynCTyped(Store&& c) : c_(std::forward<Store>(c)) {}

    Store c_;
};

/// Foo satisfies the `C` concept. It does not inherit from anything.
struct Foo final {
    void concept_fn() const noexcept { std::printf("hello world\n"); }
    void concept_fn_mut() noexcept { std::printf("hello world mut\n"); }
};
static_assert(C<Foo>);

/// These act on the `C` concept but without being templated.
void GiveC(const DynC& c) { c.concept_fn(); }
void GiveCMut(DynC& c) { c.concept_fn_mut(); }
void GiveBoxC(Box<DynC> c) { c->concept_fn(); }

template <class DynC, class ConcreteT>
    requires(std::same_as<
                 DynC, std::remove_volatile_t<std::remove_reference_t<DynC>>> &&
             std::same_as<ConcreteT, std::remove_cvref_t<ConcreteT>> &&
             !std::is_const_v<std::remove_reference_t<DynC>>)
constexpr Dyn<DynC, ConcreteT> dyn(ConcreteT& t
                                   [[clang::lifetimebound]]) noexcept {
    return Dyn<DynC, ConcreteT>(t);
}
template <class DynC, class ConcreteT>
    requires(std::same_as<
                 DynC, std::remove_volatile_t<std::remove_reference_t<DynC>>> &&
             std::same_as<ConcreteT, std::remove_cvref_t<ConcreteT>> &&
             !std::is_const_v<std::remove_reference_t<DynC>> &&
             std::is_rvalue_reference_v<ConcreteT &&>)
constexpr Dyn<DynC, ConcreteT> dyn(ConcreteT&& t
                                   [[clang::lifetimebound]]) noexcept {
    return Dyn<DynC, ConcreteT>(t);
}
template <class DynC, class ConcreteT>
    requires(std::same_as<
                 DynC, std::remove_volatile_t<std::remove_reference_t<DynC>>> &&
             std::same_as<ConcreteT, std::remove_cvref_t<ConcreteT>> &&
             std::is_const_v<std::remove_reference_t<DynC>>)
constexpr Dyn<DynC, ConcreteT> dyn(const ConcreteT& t
                                   [[clang::lifetimebound]]) noexcept {
    return Dyn<DynC, ConcreteT>(t);
}

int main() {
    Foo f1;
    GiveBoxC(Box<DynC>::from(std::move(f1)));
    auto b = Box<DynC>::from(Foo());

    Foo f2;
    GiveCMut(Dyn<DynC, Foo>(f2));
    GiveCMut(dyn<DynC>(f2));
    GiveC(Dyn<const DynC, Foo>(f2));
    GiveC(dyn<const DynC>(f2));
    GiveC(Dyn<DynC, Foo>(f2));
    GiveC(dyn<DynC>(f2));

    GiveCMut(Dyn<DynC, Foo>(Foo()));
    GiveCMut(dyn<DynC>(Foo()));
    GiveC(Dyn<const DynC, Foo>(Foo()));
    GiveC(dyn<const DynC>(Foo()));

    const Foo fc;
    GiveC(Dyn<const DynC, Foo>(fc));
    GiveC(dyn<const DynC>(fc));

    // Errors:
    // Can't make mutable Dyn from const object.
    GiveC(Dyn<DynC, Foo>(fc));
    GiveC(dyn<DynC>(fc));
    // Can't give const Dyn as mutable DynC&.
    GiveCMut(Dyn<const DynC, Foo>(f2));
    GiveCMut(dyn<const DynC>(f2));
    // Holding a reference to a temporary (on clang only).
    [[maybe_unused]] auto x = Dyn<DynC, Foo>(Foo());
    [[maybe_unused]] auto y = dyn<DynC>(Foo());
}