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'screate_reader()
member function. - On destruction, its associated
writer<>
's writing pointer is set tonullptr
.
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 associatedreader<>
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.