C#-style property in C++

10 min read Original article ↗

#pragma once

#include <concepts>

#include <filesystem>

#include <fstream>

#include <functional>

#include <initializer_list>

#include <ios>

#include <istream>

#include <iterator>

#include <memory>

#include <ostream>

#include <string>

#include <type_traits>

#include <unordered_map>

#include <utility>

#include <vector>

template<typename PT> concept std_smart_pointer = requires (PT p)

{

    typename PT::element_type;

    { static_cast<bool>(p) };

}

and std::disjunction_v<

    std::is_same<PT, std::shared_ptr<typename PT::element_type>>,

    std::is_same<PT, std::unique_ptr<typename PT::element_type>>,

    std::is_same<PT, std::shared_ptr<typename PT::element_type[]>>,

    std::is_same<PT, std::unique_ptr<typename PT::element_type[]>>>;

template<typename CT> concept std_container = requires(CT a, const CT b)

{

    requires std::regular<CT>;

    requires std::swappable<CT>;

    requires std::destructible<typename CT::value_type>;

    requires std::same_as<typename CT::reference, typename CT::value_type&>;

    requires std::same_as<typename CT::const_reference, const typename CT::value_type&>;

    requires std::forward_iterator<typename CT::iterator>;

    requires std::forward_iterator<typename CT::const_iterator>;

    requires std::signed_integral<typename CT::difference_type>;

    requires std::same_as<typename CT::difference_type, typename std::iterator_traits<typename CT::iterator>::difference_type>;

    requires std::same_as<typename CT::difference_type, typename std::iterator_traits<typename CT::const_iterator>::difference_type>;

    { a.begin() } -> std::same_as<typename CT::iterator>;

    { a.end() } -> std::same_as<typename CT::iterator>;

    { b.begin() } -> std::same_as<typename CT::const_iterator>;

    { b.end() } -> std::same_as<typename CT::const_iterator>;

    { a.cbegin() } -> std::same_as<typename CT::const_iterator>;

    { a.cend() } -> std::same_as<typename CT::const_iterator>;

    { a.size() } -> std::same_as<typename CT::size_type>;

    { a.max_size() } -> std::same_as<typename CT::size_type>;

    { a.empty() } -> std::same_as<bool>;

};

template<typename T> concept non_pointer = (not std::is_pointer_v<T>);

template<typename T> concept non_reference = (not std::is_reference_v<T>);

template<typename T> concept basic_property_type = ((non_pointer<T> and non_reference<T>) or std_smart_pointer<T> or std_container<T>);

template<basic_property_type T>

struct basic_property_policy

{

    using value_type = T;

    using reference = value_type&;

    using const_reference = const value_type&;

    using rvalue_reference = value_type&&;

    using pointer = value_type*;

    using const_pointer = const value_type*;

    basic_property_policy() = default;

    basic_property_policy(const basic_property_policy& other) : m_value(other.get()) {}

    basic_property_policy(basic_property_policy&& other) noexcept : m_value(std::move(other.get())) {}

    ~basic_property_policy() noexcept = default;

    basic_property_policy(const_reference value) : m_value(value) {}

    basic_property_policy(rvalue_reference value) : m_value(std::move(value)) {}

    template<typename U> requires (std::convertible_to<U, T>)

    basic_property_policy(U&& value) : m_value(static_cast<T>(std::forward<U>(value))) {}

    template<typename... Args> requires (std::constructible_from<T, Args...>)

    basic_property_policy(Args&&... args) : m_value(std::forward<Args>(args)...) {}

    template<typename U> requires (std::constructible_from<T, std::initializer_list<U>>)

    basic_property_policy(std::initializer_list<U> l) : m_value(l) {}

    [[nodiscard]] reference get() { return m_value; }

    [[nodiscard]] const_reference get() const { return m_value; }

    [[nodiscard]] const_reference get() const volatile { return const_cast<const_reference>(m_value); }

    void set(const_reference value) { m_value = value; }

    void set(rvalue_reference value) noexcept { m_value = std::move(value); }

private:

    T m_value;

};

template<basic_property_type T>

struct file_storage_property_policy

{

    using value_type = T;

    using reference = value_type&;

    using const_reference = const value_type&;

    using rvalue_reference = value_type&&;

    using pointer = value_type*;

    using const_pointer = const value_type*;

    explicit file_storage_property_policy(const std::filesystem::path& path) : m_path(path) {}

    file_storage_property_policy(const std::filesystem::path& path, const_reference value) : m_path(path) { set(value); }

    template<typename U = T> requires (not std::same_as<U, std::string> and not std::same_as<U, std::wstring>)

    [[nodiscard]] value_type get() const

    {

        using in_char_t = typename std::ifstream::char_type;

        auto value = T{};

        auto ifs = std::ifstream(m_path, std::ios_base::binary | std::ios_base::in);

        ifs.exceptions(std::ios::failbit | std::ios::badbit);

        ifs.read(reinterpret_cast<in_char_t*>(&value), sizeof(value));

        ifs.close();

        return value;

    }

    template<typename U = T> requires (std::same_as<U, std::string> or std::same_as<U, std::wstring>)

    [[nodiscard]] value_type get() const

    {

        using in_char_t = typename std::ifstream::char_type;

        auto value = U{};

        auto ifs = std::ifstream(m_path, std::ios_base::binary | std::ios_base::in | std::ios::ate);

        ifs.exceptions(std::ios::failbit | std::ios::badbit);

        auto bytes = ifs.tellg();

        value.resize(bytes / sizeof(typename U::value_type));

        ifs.seekg(0);

        ifs.read(reinterpret_cast<in_char_t*>(value.data()), bytes);

        ifs.close();

        return value;

    }

    template<typename U = T> requires (not std::same_as<U, std::string> and not std::same_as<U, std::wstring>)

    void set(const_reference value)

    {

        using out_char_t = typename std::ofstream::char_type;

        auto ofs = std::ofstream(m_path, std::ios_base::binary | std::ios_base::out | std::ios_base::trunc);

        ofs.exceptions(std::ios::failbit | std::ios::badbit);

        ofs.write(reinterpret_cast<const out_char_t*>(&value), sizeof(value));

        ofs.flush();

        ofs.close();

    }

    template<typename U = T> requires (std::same_as<U, std::string> or std::same_as<U, std::wstring>)

    void set(const_reference value)

    {

        using out_char_t = typename std::ofstream::char_type;

        auto ofs = std::ofstream(m_path, std::ios_base::binary | std::ios_base::out | std::ios_base::trunc);

        ofs.exceptions(std::ios::failbit | std::ios::badbit);

        ofs.write(reinterpret_cast<const out_char_t*>(value.data()), value.length() * sizeof(typename U::value_type));

        ofs.flush();

        ofs.close();

    }

private:

    std::filesystem::path m_path;

};

template<basic_property_type T, typename P>

class basic_property : private P

{

public:

    using typename P::value_type;

    using typename P::reference;

    using typename P::const_reference;

    using typename P::rvalue_reference;

    using typename P::pointer;

    using typename P::const_pointer;

    using P::P;

    using P::get;

    basic_property() = default;

    basic_property(const basic_property&) = default;

    basic_property(basic_property&&) noexcept = default;

    ~basic_property() noexcept { clear_update_proc(); }

    basic_property(const_reference value) : P(value) {}

    basic_property(rvalue_reference value) : P(std::move(value)) {}

    template<typename U> requires (std::convertible_to<U, T>)

    basic_property(U&& value) : P(std::forward<U>(value)) {}

    template<typename... Args> requires (std::constructible_from<T, Args...>)

    basic_property(Args&&... args) : P(std::forward<Args>(args)...) {}

    template<typename U> requires (std::constructible_from<T, std::initializer_list<U>>)

    basic_property(std::initializer_list<U> l) : P(l) {}

    basic_property& operator = (const basic_property& other)

    {

        P::set(other.get());

        dispatch_update();

        return *this;

    }

    basic_property& operator = (basic_property&& other) noexcept

    {

        P::set(std::move(other.get()));

        dispatch_update();

        return *this;

    }

    basic_property& operator = (const_reference value)

    {

        P::set(value);

        dispatch_update();

        return *this;

    }

    basic_property& operator = (rvalue_reference value) noexcept

    {

        P::set(std::move(value));

        dispatch_update();

        return *this;

    }

    template<typename U> requires (std::constructible_from<T, U>

    and not std::same_as<T, std::remove_cvref_t<U>> and not std::same_as<basic_property, std::remove_cvref_t<U>>)

    basic_property& operator = (U&& value)

    {

        P::set(std::forward<U>(value));

        dispatch_update();

        return *this;

    }

    [[nodiscard]] auto operator <=> (const basic_property& other) const { return P::get() <=> other.get(); }

    [[nodiscard]] bool operator == (const basic_property& other) const { return P::get() == other.get(); }

    template<typename U> requires (not std::same_as<basic_property, std::remove_cvref_t<U>>)

    [[nodiscard]] auto operator <=> (const U& other) const { return P::get() <=> other; }

    template<typename U> requires (not std::same_as<basic_property, std::remove_cvref_t<U>>)

    [[nodiscard]] bool operator == (const U& other) const { return P::get() == other; }

#if defined(DEFINE_PROPERTY_OPERATOR)

#undef DEFINE_PROPERTY_OPERATOR

#endif

#define DEFINE_PROPERTY_OPERATOR(OP) \

    basic_property& operator OP (const basic_property& other) \

    { \

        P::get() OP other.get(); \

        dispatch_update(); \

        return *this; \

    } \

    \

    template<typename U> requires (not std::same_as<basic_property, std::remove_cvref_t<U>>) \

    basic_property& operator OP (const U& value) \

    { \

        P::get() OP value; \

        dispatch_update(); \

        return *this; \

    }

    DEFINE_PROPERTY_OPERATOR(+=);

    DEFINE_PROPERTY_OPERATOR(-=);

    DEFINE_PROPERTY_OPERATOR(*=);

    DEFINE_PROPERTY_OPERATOR(/=);

    DEFINE_PROPERTY_OPERATOR(%=);

    DEFINE_PROPERTY_OPERATOR(&=);

    DEFINE_PROPERTY_OPERATOR(|=);

    DEFINE_PROPERTY_OPERATOR(^=);

    DEFINE_PROPERTY_OPERATOR(<<=);

    DEFINE_PROPERTY_OPERATOR(>>=);

#undef DEFINE_PROPERTY_OPERATOR

    explicit operator reference () { return P::get(); }

    operator const_reference () const { return P::get(); }

    operator const_reference () const volatile { return P::get(); }

    template<typename PT = T> requires (not std_smart_pointer<PT>)

    [[nodiscard]] pointer operator & () { return &P::get(); }

    template<typename PT = T> requires (not std_smart_pointer<PT>)

    [[nodiscard]] const_pointer operator & () const { return &P::get(); }

    template<typename PT = T> requires (not std_smart_pointer<PT>)

    [[nodiscard]] const_pointer operator & () const volatile { return &P::get(); }

    template<typename PT = T> requires (not std_smart_pointer<PT>)

    [[nodiscard]] reference operator * () { return P::get(); }

    template<typename PT = T> requires (not std_smart_pointer<PT>)

    [[nodiscard]] const_reference operator * () const { return P::get(); }

    template<typename PT = T> requires (not std_smart_pointer<PT>)

    [[nodiscard]] const_reference operator * () const volatile { return P::get(); }

    template<typename PT = T> requires (not std_smart_pointer<PT>)

    pointer operator -> () { return &P::get(); }

    template<typename PT = T> requires (not std_smart_pointer<PT>)

    const_pointer operator -> () const { return &P::get(); }

    template<typename PT = T> requires (not std_smart_pointer<PT>)

    const_pointer operator -> () const volatile { return &P::get(); }

    template<typename PT = T> requires (std_smart_pointer<PT>)

    explicit operator bool() const noexcept { return static_cast<bool>(P::get()); }

    template<typename PT = T> requires (std_smart_pointer<PT>)

    [[nodiscard]] PT::element_type* operator & () { return P::get().get(); }

    template<typename PT = T> requires (std_smart_pointer<PT>)

    [[nodiscard]] const PT::element_type* operator & () const { return P::get().get(); }

    template<typename PT = T> requires (std_smart_pointer<PT>)

    [[nodiscard]] const PT::element_type* operator & () const volatile { return P::get().get(); }

    template<typename PT = T> requires (std_smart_pointer<PT>)

    [[nodiscard]] PT::element_type& operator * () { return *P::get(); }

    template<typename PT = T> requires (std_smart_pointer<PT>)

    [[nodiscard]] const PT::element_type& operator * () const { return *P::get(); }

    template<typename PT = T> requires (std_smart_pointer<PT>)

    [[nodiscard]] const PT::element_type& operator * () const volatile { return *P::get(); }

    template<typename PT = T> requires (std_smart_pointer<PT>)

    reference operator -> () { return P::get(); }

    template<typename PT = T> requires (std_smart_pointer<PT>)

    const_reference operator -> () const { return P::get(); }

    template<typename PT = T> requires (std_smart_pointer<PT>)

    const_reference operator -> () const volatile { return P::get(); }

    template<typename U> requires (std_smart_pointer<T> or std_container<T>)

    [[nodiscard]] decltype(auto) operator [] (U&& index) { return P::get()[std::forward<U>(index)]; }

    template<typename U> requires (std_smart_pointer<T> or std_container<T>)

    [[nodiscard]] decltype(auto) operator [] (U&& index) const { return P::get()[std::forward<U>(index)]; }

    template<typename U> requires (std_smart_pointer<T> or std_container<T>)

    [[nodiscard]] decltype(auto) operator [] (U&& index) const volatile { return P::get()[std::forward<U>(index)]; }

    template<typename C = T> requires (std_container<C>)

    [[nodiscard]] decltype(auto) begin() { return P::get().begin(); }

    template<typename C = T> requires (std_container<C>)

    [[nodiscard]] decltype(auto) end() { return P::get().end(); }

    template<typename C = T> requires (std_container<C>)

    [[nodiscard]] decltype(auto) begin() const { return P::get().begin(); }

    template<typename C = T> requires (std_container<C>)

    [[nodiscard]] decltype(auto) end() const { return P::get().end(); }

    template<typename C = T> requires (std_container<C>)

    [[nodiscard]] decltype(auto) cbegin() { return P::get().cbegin(); }

    template<typename C = T> requires (std_container<C>)

    [[nodiscard]] decltype(auto) cend() { return P::get().cend(); }

    template<typename Func, typename... Args> requires (std::invocable<Func, T, Args...>)

    auto invoke(Func&& func, Args&&... args) -> std::invoke_result_t<Func, T, Args...>

    {

        return std::invoke(std::forward<Func>(func), P::get(), std::forward<Args>(args)...);

    }

    template<typename Func, typename... Args> requires (std::invocable<Func, T, Args...>)

    auto invoke(Func&& func, Args&&... args) const -> std::invoke_result_t<Func, T, Args...>

    {

        return std::invoke(std::forward<Func>(func), P::get(), std::forward<Args>(args)...);

    }

    template<typename Func, typename... Args> requires (std::invocable<Func, T, Args...>)

    auto invoke(Func&& func, Args&&... args) const volatile -> std::invoke_result_t<Func, T, Args...>

    {

        return std::invoke(std::forward<Func>(func), P::get(), std::forward<Args>(args)...);

    }

    template<typename Proc> requires (std::invocable<Proc, basic_property, void*>)

    void set_update_proc(Proc&& proc, void* ctx) { k_update_proc_map.insert_or_assign(this, std::make_pair(std::forward<Proc>(proc), ctx)); }

    void clear_update_proc() noexcept { k_update_proc_map.erase(this); }

    friend std::ostream& operator << (std::ostream& output, const basic_property& prop)

    {

        output << prop.get();

        return output;

    }

    friend std::wostream& operator << (std::wostream& output, const basic_property& prop)

    {

        output << prop.get();

        return output;

    }

    friend std::istream& operator >> (std::istream& input, basic_property& prop)

    {

        input >> prop.get();

        prop.dispatch_update();

        return input;

    }

    friend std::wistream& operator >> (std::wistream& input, basic_property& prop)

    {

        input >> prop.get();

        prop.dispatch_update();

        return input;

    }

private:

    using update_proc_t = std::function<void(const basic_property&, void*)>;

    using update_proc_map_key_t = void*;

    using update_proc_map_val_t = std::pair<update_proc_t, void*>;

    using update_proc_map_t = std::unordered_map<update_proc_map_key_t, update_proc_map_val_t>;

    inline static auto k_update_proc_map = update_proc_map_t{};

    void dispatch_update() noexcept

    {

        if (auto it = k_update_proc_map.find(this); it != std::end(k_update_proc_map))

        {

            auto& proc = it->second.first;

            auto& ctx = it->second.second;

            proc(*this, ctx);

        }

    }

};

basic_property(const char*) -> basic_property<std::string, basic_property_policy<std::string>>;

basic_property(const wchar_t*) -> basic_property<std::wstring, basic_property_policy<std::wstring>>;

template<typename T> basic_property(T*) -> basic_property<std::unique_ptr<T>, basic_property_policy<std::unique_ptr<T>>>;

template<typename Iterator> basic_property(Iterator, Iterator) ->

    basic_property<std::vector<typename std::iterator_traits<Iterator>::value_type>,

    basic_property_policy<std::vector<typename std::iterator_traits<Iterator>::value_type>>>;

template<basic_property_type T, typename P = basic_property_policy<T>>

[[nodiscard]] auto make_property(const basic_property<T, P>& prop)

{

    using U = std::remove_cvref_t<T>;

    return basic_property<U, P>(prop);

}

template<basic_property_type T, typename P = basic_property_policy<T>>

[[nodiscard]] auto make_property(basic_property<T, P>&& prop)

{

    using U = std::remove_cvref_t<T>;

    return basic_property<U, P>(std::move(prop));

}

template<basic_property_type T, typename P = basic_property_policy<T>>

[[nodiscard]] auto make_property(const T& value)

{

    using U = std::remove_cvref_t<T>;

    return basic_property<U, P>(value);

}

template<basic_property_type T, typename P = basic_property_policy<T>>

[[nodiscard]] auto make_property(T&& value)

{

    using U = std::remove_cvref_t<T>;

    return basic_property<U, P>(std::move(value));

}

template<basic_property_type T, typename U, typename P = basic_property_policy<T>>

requires (std::convertible_to<U, T>)

[[nodiscard]] auto make_property(U&& value)

{

    using V = std::remove_cvref_t<T>;

    return basic_property<V, P>(std::forward<U>(value));

}

template<basic_property_type T, typename... Args, typename P = basic_property_policy<T>>

requires (std::constructible_from<T, Args...>)

[[nodiscard]] auto make_property(Args&&... args)

{

    using U = std::remove_cvref_t<T>;

    return basic_property<U, P>(std::forward<Args>(args)...);

}

template<basic_property_type T, typename U, typename P = basic_property_policy<T>>

requires (std::constructible_from<T, std::initializer_list<U>>)

[[nodiscard]] auto make_property(std::initializer_list<U> l)

{

    using V = std::remove_cvref_t<T>;

    return basic_property<V, P>(l);

}

[[nodiscard]] auto make_property(const char* str)

{

    return basic_property(str);

}

[[nodiscard]] auto make_property(const wchar_t* str)

{

    return basic_property(str);

}

template<typename T>

requires (std::is_pointer_v<T>)

[[nodiscard]] auto make_property(T ptr)

{

    return basic_property(ptr);

}

template<typename Iterator>

requires (std::input_iterator<Iterator>)

[[nodiscard]] auto make_property(Iterator begin, Iterator end)

{

    return basic_property(begin, end);

}

template<typename T, typename P>

[[nodiscard]] decltype(auto) strip(basic_property<T, P>& prop)

{

    return static_cast<T&>(prop);

}

template<typename T, typename P>

[[nodiscard]] decltype(auto) strip(const basic_property<T, P>& prop)

{

    return static_cast<const T&>(prop);

}

template<typename T, typename P>

[[nodiscard]] decltype(auto) strip(const volatile basic_property<T, P>& prop)

{

    return static_cast<const T&>(prop);

}

template<typename T, typename P>

[[nodiscard]] const volatile basic_property<T, P>& as_volatile(const basic_property<T, P>& prop)

{

    return const_cast<const volatile basic_property<T, P>&>(prop);

}

#if defined(DEFINE_PROPERTY_OPERATOR_UNARY)

#undef DEFINE_PROPERTY_OPERATOR_UNARY

#endif

#define DEFINE_PROPERTY_OPERATOR_UNARY(PT, OP) \

    template<typename T> \

    [[nodiscard]] auto operator OP (const PT<T>& prop) -> \

        PT<decltype(OP std::declval<T>())> \

            { return { OP prop.get() }; }

#if defined(DEFINE_PROPERTY_OPERATOR_BINARY)

#undef DEFINE_PROPERTY_OPERATOR_BINARY

#endif

#define DEFINE_PROPERTY_OPERATOR_BINARY(PT, OP) \

    template<typename T1, typename T2> \

    [[nodiscard]] auto operator OP (const PT<T1>& lhs, const PT<T2>& rhs) -> \

        PT<decltype(std::declval<T1>() OP std::declval<T2>())> \

            { return { lhs.get() OP rhs.get() }; } \

    \

    template<typename T, typename U> requires (not std::same_as<PT<T>, std::remove_cvref_t<U>>) \

    [[nodiscard]] auto operator OP (const PT<T>& lhs, const U& rhs) -> \

        PT<decltype(std::declval<T>() OP std::declval<U>())> \

            { return { lhs.get() OP rhs }; } \

    \

    template<typename U, typename T> requires (not std::same_as<std::remove_cvref_t<U>, PT<T>> \

    and not std::same_as<std::remove_cvref_t<U>, std::ostream> and not std::same_as<std::remove_cvref_t<U>, std::wostream> \

    and not std::same_as<std::remove_cvref_t<U>, std::istream> and not std::same_as<std::remove_cvref_t<U>, std::wistream>) \

    [[nodiscard]] auto operator OP (const U& lhs, const PT<T>& rhs) -> \

        PT<decltype(std::declval<U>() OP std::declval<T>())> \

            { return { lhs OP rhs.get() }; }

template<basic_property_type T> using property = basic_property<T, basic_property_policy<T>>;

DEFINE_PROPERTY_OPERATOR_UNARY(property, +);

DEFINE_PROPERTY_OPERATOR_UNARY(property, -);

DEFINE_PROPERTY_OPERATOR_BINARY(property, +);

DEFINE_PROPERTY_OPERATOR_BINARY(property, -);

DEFINE_PROPERTY_OPERATOR_BINARY(property, *);

DEFINE_PROPERTY_OPERATOR_BINARY(property, /);

DEFINE_PROPERTY_OPERATOR_BINARY(property, %)

DEFINE_PROPERTY_OPERATOR_UNARY(property, ~);

DEFINE_PROPERTY_OPERATOR_BINARY(property, &);

DEFINE_PROPERTY_OPERATOR_BINARY(property, |);

DEFINE_PROPERTY_OPERATOR_BINARY(property, ^);

DEFINE_PROPERTY_OPERATOR_BINARY(property, <<);

DEFINE_PROPERTY_OPERATOR_BINARY(property, >>);

template<basic_property_type T> using fs_property = basic_property<T, file_storage_property_policy<T>>;

DEFINE_PROPERTY_OPERATOR_UNARY(fs_property, +);

DEFINE_PROPERTY_OPERATOR_UNARY(fs_property, -);

DEFINE_PROPERTY_OPERATOR_BINARY(fs_property, +);

DEFINE_PROPERTY_OPERATOR_BINARY(fs_property, -);

DEFINE_PROPERTY_OPERATOR_BINARY(fs_property, *);

DEFINE_PROPERTY_OPERATOR_BINARY(fs_property, /);

DEFINE_PROPERTY_OPERATOR_BINARY(fs_property, %)

DEFINE_PROPERTY_OPERATOR_UNARY(fs_property, ~);

DEFINE_PROPERTY_OPERATOR_BINARY(fs_property, &);

DEFINE_PROPERTY_OPERATOR_BINARY(fs_property, |);

DEFINE_PROPERTY_OPERATOR_BINARY(fs_property, ^);

DEFINE_PROPERTY_OPERATOR_BINARY(fs_property, <<);

DEFINE_PROPERTY_OPERATOR_BINARY(fs_property, >>);

#undef DEFINE_PROPERTY_OPERATOR_UNARY

#undef DEFINE_PROPERTY_OPERATOR_BINARY