static_assert( __cplusplus > 2020'99 ); #pragma once #include #include #include #include #include #include #include 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 > &¶ms ) { build_streambuf( os, std::move( params ) ); return os; } template< typename T > std::istream & operator >> ( std::istream &is, PushStack< T > &¶ms ) { 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 > &¶ms, std::ostream &os ) { return AutoRAII { [&os, ¶ms] { build_streambuf( os, std::move( params ) ); }, [&os] { os << PopStack{}; } }; } } namespace Alepha::Hydrogen::IOStreams::inline exports::inline StackableStreambuf_m { using namespace detail::StackableStreambuf_m::exports; }