2
\$\begingroup\$

While extending the functionality of my polymorphic_callable<> type, I designed a way to obtain a result through a reader type that is set by a writer type; this is very similar to what std::future<> and std::promise<> do, but in a non-multi-threaded way.

Since that other question is already very long, I've decided to post this question for a review that focuses on these simple classes.


Reader

reader<> semantics:

  • It can only read from its contained value.
  • It is a move-only type that can only be created by a writer<> instance's create_reader() member function.
  • On destruction, its associated writer<>'s writing pointer is set to nullptr.

reader.h

#ifndef READER_H
#define READER_H

#include <utility>
#include <type_traits>

template<class> class writer;

template<class T>
class reader
{
public:
    using value_type = T;

    /**
    * @brief sets the associated writer's pointer to nullptr
    */
    ~reader() noexcept( std::is_nothrow_destructible<value_type>::value )
    {
        writer_->value_ = nullptr;
    }

    /**
    * @brief takes ownership of the contained value of the argument reader as
    * if by move construction; associates the writer associated with the argument
    * reader with this reader.
    */
    reader( reader&& rhs )
    noexcept( std::is_nothrow_move_constructible<value_type>::value )
        : value_{ std::move( rhs.value_ ) }
        , writer_{ rhs.writer_ }
    {
        rhs.writer_->value_ = &value_;
    }

    /**
    * @brief takes ownership of the contained value of the argument reader as
    * if by move assignment; associates the writer associated with the argument
    * reader with this reader.
    */
    reader& operator=( reader&& rhs )
    noexcept( std::is_nothrow_move_assignable<value_type>::value )
    {
        writer_ = rhs.writer_;
        value_ = std::move( rhs.value_ );
        rhs.writer_->value_ = &value_;
    }

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

    /**
    * @brief access the contained value
    */
    value_type const& value() const noexcept
    {
        return value_;
    }

private:
    friend class writer<value_type>;

    /**
     * @brief constructs a reader and associates it with the parameter writer
     */
    reader( writer<value_type>* w )
        noexcept( std::is_nothrow_default_constructible<value_type>::value )
        : value_{}
        , writer_{ w }
    {
        w->value_ = &value_;
    }

    value_type value_;
    writer<value_type>* writer_;
};

template<>
class reader<void>
{
public:
    using value_type = void;
};
#endif // READER_H

Writer

writer<> semantics:

  • It is a friend class of the writer<> type.
  • It has a pointer to its associated reader<> instance.
  • It can write to its associated reader; if it's associated reader is not set, then writing does nothing. This is because a writer<> instance can outlive its associated reader<> instance.
  • It can be queried to know whether it has an associated reader or not with the has_reader() member function.

writer.h

#ifndef WRITER_H
#define WRITER_H

#include "reader.h"
#include <utility>
#include <type_traits>

template<class T>
class writer
{
public:
    using value_type = T;

    ~writer() = default;

    /**
    * @brief constructs a writer with no assicated reader.
    */
    constexpr writer() noexcept
        : value_{ nullptr }
    {}

    writer( writer&& rhs ) = default;
    writer( writer const& ) = default;
    writer& operator=( writer&& ) = default;
    writer& operator=( writer const& ) = default;

    /**
    * @brief sets the value associated with this writer; if there is no associated
    * value, the function does nothing.
    */
    template<class... Args>
    void set_value( Args&&... args )
    noexcept( std::is_nothrow_constructible<value_type, Args&&...>::value )
    {
        if ( value_ )
        {
            *value_ = value_type{ std::forward<Args>( args )... };
        }
    }

    /**
    * @brief creates a new reader and associates it to the reader.
    */
    reader<value_type> create_reader() noexcept
    {
        return reader<value_type>{ this };
    }

    /**
    * @brief indicates whether the reader has an associated writer or not.
    * @return true if the reader has an associated writer, false otherwise.
    */
    bool has_reader() const noexcept
    {
        return static_cast<bool>( value_ );
    }

private:
    friend class reader<value_type>;

    value_type* value_;
};

template<>
class writer<void>
{
public:
    using value_type = void;

private:
    friend class reader<value_type>;
};
#endif // WRITER_H

While the true use case of these types is found here, here is an usage sample:

#include <iostream>

int main()
{
    writer<int> w;
    reader<int> r{ w.create_reader() };
    w.set_value( 3 );
    std::cout << r.value() << '\n';
}

Additional usage sample:

struct updated_t
{
    updated_t( writer<int>& wint, writer<std::string>& wstring )
        : rint{ wint.create_reader() }
        , rstring{ wstring.create_reader() }
    {}

    reader<int> rint;
    reader<std::string> rstring;
};

int main()
{
    writer<int> wint;
    writer<std::string> wstring;
    updated_t updated{ wint, wstring };
    while ( true )
    {
        /* write with wint, wstring */
        /* rint and rstring get updated where ever they may be */
        wint.set_value( 3 );
        std::cout << updated.rint.value() << '\n';

        wstring.set_value( "hello world\n" );
        std::cout << updated.rstring.value() << '\n';

        break;
    }
}

I'm interested in a review on design and any other features that are missing or that could be added/improved.

\$\endgroup\$
1
\$\begingroup\$

Easy of use:

You could make implicit conversion to bool for writer<> class, so it would be less cumbersome to use it. On the other hand, those who are not familiar with the internals might be surprised during their first use.

Since classes has explicit names, you could write operator(), since it is explicit that invoking it on reader<> object will perform read, and invoking it with parameter on writer<> object will perform writing.

Matter to think about:

The code uses circular references. I know that it greatly simplifies the implementation, but probably there is a better solution. I think that the problem leads to creation of raw memory class, which would be supplied into the polymorphic_callable<>. std::optional<> would be great idea too. When std::optional<> object will evaluate to true, it will mean that the function has been called and it properly finished execution. You could provide your own until C++17, since it is pretty trivial to write one.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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