1
0
forked from Alepha/Alepha
Files
Alepha/IOStreams/StackableStreambuf.h

223 lines
5.9 KiB
C++

static_assert( __cplusplus > 2020'99 );
#pragma once
#include <Alepha/Alepha.h>
#include <ostream>
#include <exception>
#include <stdexcept>
#include <memory>
#include <stack>
#include <Alepha/AutoRAII.h>
namespace Alepha::Hydrogen::IOStreams ::detail:: StackableStreambuf_m
{
inline namespace exports
{
struct StackableStreambuf;
template< typename > struct PushStack;
struct PopStack{};
std::size_t stackDepth( std::ios & );
}
struct StackableStreambufUnderlyingShim
: protected virtual std::streambuf
{
private:
virtual std::streambuf *getUnderlying() const= 0;
protected:
// EEEW. But to sidestep/circumvent, all this below is necessary.
auto
forwardOverflow( const int ch )
{
int (std::streambuf::*ov)( int )= &StackableStreambufUnderlyingShim::overflow;
return ( getUnderlying()->*ov )( ch );
}
auto
forwardUnderflow()
{
int (std::streambuf::*uf)()= &StackableStreambufUnderlyingShim::underflow;
return (getUnderlying()->*uf)();
}
auto
underlying_sbumpc() const
{
int (std::streambuf::*sbc)()= &StackableStreambufUnderlyingShim::sbumpc;
return (getUnderlying()->*sbc)();
}
auto
underlying_eback() const
{
char *(std::streambuf::*eb)() const= &StackableStreambufUnderlyingShim::eback;
return (getUnderlying()->*eb)();
}
auto
underlying_gptr() const
{
char *(std::streambuf::*gp)() const= &StackableStreambufUnderlyingShim::gptr;
return (getUnderlying()->*gp)();
}
auto
underlying_egptr() const
{
char *(std::streambuf::*egp)() const= &StackableStreambufUnderlyingShim::egptr;
return (getUnderlying()->*egp)();
}
void
underlying_setg( char *const b, char *const g, char *const e )
{
void (std::streambuf::*sg)( char *, char *, char * )= &StackableStreambufUnderlyingShim::setg;
return (getUnderlying()->*sg)( b, g, e );
}
auto
underlying_pbase() const
{
char *(std::streambuf::*pb)() const= &StackableStreambufUnderlyingShim::pbase;
return (getUnderlying()->*pb)();
}
auto
underlying_pptr() const
{
char *(std::streambuf::*pp)() const= &StackableStreambufUnderlyingShim::pptr;
return (getUnderlying()->*pp)();
}
auto
underlying_epptr() const
{
char *(std::streambuf::*ep)() const= &StackableStreambufUnderlyingShim::epptr;
return (getUnderlying()->*ep)();
}
};
struct exports::StackableStreambuf
: virtual public std::streambuf, virtual protected StackableStreambufUnderlyingShim
{
private:
std::streambuf *underlying;
std::streambuf *getUnderlying() const final { return underlying; }
public:
~StackableStreambuf() override;
// Children must be created by `new`.
explicit StackableStreambuf( std::ios &host );
virtual void writeChar( char ch )= 0;
virtual void drain()= 0;
void unhook( std::ostream &os ) const;
void unhook( std::istream &is ) const;
protected:
std::ostream &out( std::ostream &&res= std::ostream{ nullptr } ) const;
int overflow( const int ch ) override;
// Underflow is not overridden. A read-oriented
// streambuf stack may choose to implement underflow.
//
// The reason underflow is not adapted to a
// char-interceptor is that memory usage would become
// unbound. For overflow/writing, we can assume
// that sufficient output storage space exists for
// whatever transformations a single char turns into.
// For instance, a decompression algorithm might
// cause several kilobytes of data to be generated
// once a single character is written. These
// kilobytes must be immediately stored deeper into
// the stack. These writes can be fire-and-forget,
// as each lower level will have to deal with and
// store those bytes (and potential further
// expansions or reductions). Writes are conveniently
// fire-and-forget.
//
// Whereas, for input, when converting a single input
// char from the lower level, a reduction would simply
// require subsequent reads proxied by the stack.
// However, expansions would potentially emit large
// blocks of data which would have to be cached
// somewhere. The intermediate state of the expansion
// generator cannot be suspended and resumed -- it's
// a function, not a coroutine. Thus, the entirety
// of the expansion routine has to be drained into
// a lookaside buffer to permit it to run to completion.
// This buffer has to be held in memory, which can
// grow without bound. Whereas, with writes, we can
// block this generator on downstream space being
// available. Yet reads require the upstream has
// to be given a chance to handle what we've seen so
// far in order to make more in-memory buffer space
// available.
std::streamsize xsputn( const char *data, std::streamsize amt ) override;
void
assume_underlying()
{
setg( underlying_gptr(), underlying_gptr(), underlying_egptr() );
underlying_setg( egptr(), egptr(), egptr() );
}
};
template< typename T >
struct exports::PushStack
: T
{
using T::T;
};
template< typename T >
std::ostream &
operator << ( std::ostream &os, PushStack< T > &&params )
{
build_streambuf( os, std::move( params ) );
return os;
}
template< typename T >
std::istream &
operator >> ( std::istream &is, PushStack< T > &&params )
{
build_streambuf( is, std::move( params ) );
return is;
}
inline namespace impl
{
std::ostream & operator << ( std::ostream &os, PopStack );
std::istream & operator >> ( std::istream &is, PopStack );
}
template< typename T >
[[nodiscard]] auto
adaptStream( PushStack< T > &&params, std::ostream &os )
{
return AutoRAII
{
[&os, &params] { build_streambuf( os, std::move( params ) ); },
[&os] { os << PopStack{}; }
};
}
}
namespace Alepha::Hydrogen::IOStreams::inline exports::inline StackableStreambuf_m
{
using namespace detail::StackableStreambuf_m::exports;
}