tfc / pprintpp

Typesafe Python Style Printf Formatting for C++
MIT License
233 stars 12 forks source link

How to integrate with user defined types #10

Closed TorstenRobitzki closed 5 years ago

TorstenRobitzki commented 5 years ago

I wonder, if it would be possible to add support for user defined types and if so, how. Or if not, where would one add support for user defined types. Ideas?

struct foo {
} my_foo;

std::size_t print( char* buffer, std::size_t buffer_size, const foo& );
pprintpp( "This is my foo: {}", my_foo );

Could we somehow extend pprintpp to have print() invoked with some statically allocated buffer for formatting purpose?

I know, pprintpp() is "just" creating printf style format strings, but in real world applications, user defined types have to be added sooner or later.

DBJDBJ commented 5 years ago

Well, there is this ancient school of philosophy that claims: "nothing must break encapsulation and decoupling" ...

  pprint(" Hey Ma, Look: { %foo } !", _foo_instance ) ;

They go as far as claiming above "integration" naturally breaks both in one go, for the foo type... I might be sure they would advise on this course of action:

   pprint("Hey, Ma, look: { }!",   foo::to_string( _foo_instance ) ) ;

Complete decoupling and complete encapsulation ... Silly people ...

TorstenRobitzki commented 5 years ago

Well, this requires some kind of std::string. Being in the embedded world, providing a statically allocated buffer, that can be used for multiple invocations of to_string could take the buffer and "consume" portion of it by taking it by reference. Something like this:

char buffer[100];
auto buf = create_consumable( buffer );
pprint("Multiple Foos: {} / {}", to_string( buf, foo1 ), to_string( buf, foo2 ) );

Which of cause won't work as both calls to to_string() must have a side effect on buf, but the order of evaluating would be undefined.

Thanks for your suggestion.

TorstenRobitzki commented 5 years ago

and of cause, it requires some kind of support for std::string in pprint.

DBJDBJ commented 5 years ago

Encapsulation and decoupling. That is a fundamental postulate. Not a "suggestion".

There is a rumor some of them ancients have developed ancient parts of std:: twilight zone. Your line of thinking is not that uncommon. Whan I aksed that question, in particular std::exception has been brought to my meek persona attention, so I can learn by example:

// two instances of some offsprings of std::exception
// x1 and x2, came in
pprintp("Look Ma, no integration and no std::strings ! {s} {s} ",  x1.what(), x2.what() ) ;

They actully use const char * ... Silly people ...

tfc commented 5 years ago

@TorstenRobitzki i am glad that the language turns out to be useful in your project! I keep seeing new issue items very late because for some reason i don't get emails about it.

I think the cleanest way to extend this is this:

Is this something that might work for you?

pprintpp is simply a format string preprocessor that creates no runtime code. Thinking about different models might be possible, but i guess we would need kind of rethink the whole thing from the beginning (which would be fun of course). This is why i suggest this approach with separated concerns.

TorstenRobitzki commented 5 years ago

Yes, we are fully aware, that pprintpp "just" turns a typelist and a format string in a new format, but we though, that we are probably not the only and first one, that have the requirements to use user defined types. As we are basically using snprintf() and not printf(), we wrapped snprintf() already to have some formatting like this:

char buffer[ 100 ];
const std::size_t buffer_used  = format( buffer, "{s} = {}\r\n", "Elf", 11 );

I though about your approach already, but rejected the idea, because it inverses the usual library dependencies (in addition, we have printf() in conan package that would then be under constant change).

I'm currently thinking of wrapping buffer in some kind of type to select a different overload of snprintf() and to change pprintf() to check if unsupported types (aka user defined) type support a specific overloaded function, that could be used to pass the buffer and the object to, to fill the buffer.

I also try to understand, if pprintpp would still be the right place to start with or if it might be easier to start from scratch.

DBJDBJ commented 5 years ago

@TorstenRobitzki first I hope you do not mind my approach before. :)

Your project has requirements which we do not know but, I really do not see what might be wrong with:

// just a sketch
struct Foo {
    whatever data_ ;
    const char * to_string () const {
        // 1. somehow put data_ string presentation to this->buffy_
        // 2. return the native pointer to it
        return this->buffy_ ;
    };
    private: 
      char buffy_[BUFSIZ]{char(0)};
} ;

 // ... somewhere else
void test ( Foo const & foo_ )  {
    pprintf(" Foo instance: { s } \n\n",  foo_.to_string() ) ;
} 

pprintf is simple, effective and feasible as it is ... Perhaps we should think of it as we think of stdio.h printf() : it is there it is useful and it is unchangeable :)

TorstenRobitzki commented 5 years ago

Your project has requirements which we do not know but, I really do not see what might be wrong with:

Well, having a buffer inside every data type that has to have a string representation is simply overkill and it won't work for enumerations and it requires support by the user defined type, which might be unsuitable for types from existing libraries.

tfc commented 5 years ago

It seems like it would be quite helping in your case if your printf-implementation would recognize "callable" arguments. It could then call them when they are needed and they serialize themselves into a buffer (and they all could use the same one), or something like that. But that is on a layer that is completely off from where pprintpp works... I see that it's tempting to do something in pprintpp, but it's hard to see where this would really fit when thinking this through.

DBJDBJ commented 5 years ago

@TorstenRobitzki , The point is in the last sentence :

pprintf is simple, effective and feasible as it is ... Perhaps we should think of it, as we think of stdio.h printf() : it is there it is useful and it is unchangeable :)

Yes ... "callable arguments" is one OK way of doing it. It does not change pprintf ... There are many C++ variadic arguments based designs, working that way, for example...

TorstenRobitzki commented 5 years ago

Thanks for the open discussion. I will think about this a little bit longer, but I think the solution to our requirements are somewhere outside of pprintfpp.

DBJDBJ commented 5 years ago

@TorstenRobitzki You might consider this schema

#include "../pprintpp.hpp"
#include <cassert>
#include <cstdio>
#include <memory>

using namespace std;
/*
somewhat inspired with
https://msdn.microsoft.com/en-us/magazine/dn913181.aspx
*/
template <typename T>
inline T frm_arg(T value) noexcept
{
    return value;
}

template<typename ... Args>
inline auto to_buff
(char const* fmt, Args ...args)
noexcept -> unique_ptr<char[]>
{
    assert(fmt != nullptr);
    // 1: what is the size required
    size_t size = 1 + std::snprintf(nullptr, 0, fmt, frm_arg(args) ...);
    assert(size > 0);
    // 2: use it at runtime
    auto buf = std::make_unique<char[]>(size + 1);
    size = std::snprintf(buf.get(), size, fmt, frm_arg(args) ...);
    assert(size > 0);
    return buf;
}
///////////////////////////////////////////////////////////////////////
struct Foo {
    const char* name = "Foo";
};

inline constexpr char const * frm_arg(Foo const & foo_ ) noexcept
{
    return foo_.name;
}
///////////////////////////////////////////////////////////////////////
extern "C" int test_4(int, wchar_t* [])
{
    auto rez{ to_buff(
        "\n\nFoo: %s, Foo: %s, %s %d\n\n", Foo{"A"}, Foo{"B"}, "The answer is:", 42
      )};
    pprintf("{s}", rez.get() );
    return EXIT_SUCCESS;
}