Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

More succinct way to use shims in variadic templates?

C++ templates are generally assimilated to creators of bloat, and the Shim idea deals with exactly that: making the template just a thin wrapper over a regular function. It's a really great way to cut down on the bloat.

For example, let's use a simple shim:

//
// Shim interface
//
struct Interface {
    virtual void print(std::ostream& out) const = 0;
}; // struct Interface

std::ostream& operator<<(std::ostream& out, Interface const& i) {
    i.print(out);
    return out;
}

template <typename T>
struct IT: public Interface {
    IT(T const& t): _t(t) {}
    virtual void print(std::ostream& out) const { out << _t; }
    T const& _t;
};

template <typename T>
IT<T> shim(T const& t) { return IT<T>(t); }

Now, I can use it like so:

void print_impl(Interface const& t);

template <typename T>
void print(T const& t) { print_impl(shim(t)); }

And no matter how print_impl is implemented, print remains very lightweight and should be inlined. Easy peasy.


C++11 however introduces variadic templates. The typical urge then is to reimplement all unsafe C-variadics with C++11 variadic templates, even Wikipedia suggests so with a printf implementation.

Unfortunately, Wikipedia's implementation does not deal with positional arguments: the kind that allows you to specify print the 3rd parameter there, etc... It would be easy, if only we had a function with this prototype:

void printf_impl(char const* format, Interface const* array, size_t size);

or similar.

Now, how do we bridge from the original interface:

template <typename... T>
void printf(char const* format, T const&... t);

to the signature above ?

One difficulty with the shims is that they rely on the binding to const-ref behavior to extend the lifetime of the temporary wrapper created just enough without having to allocate memory dynamically (they would not be cheap if they did).

It seems difficult though to get that binding + the array transformation in one step. Especially because arrays of references (and pointer to references) are not allowed in the language.


I have a beginning of a solution, for those interested:

//
// printf (or it could be!)
//
void printf_impl(char const*, Interface const** array, size_t size) {
    for (size_t i = 0; i != size; ++i) { std::cout << *(array[i]); }
    std::cout << "\n";
}

template <typename... T>
void printf_bridge(char const* format, T const&... t) {
    Interface const* array[sizeof...(t)] = { (&t)... };
    printf_impl(format, array, sizeof...(t));
}

template <typename... T>
void printf(char const* format, T const&... t) {
    printf_bridge(format, ((Interface const&)shim(t))...);
}

however you will note the introduction of a supplementary step, which is a bit annoying. Still, it appears to work.

I would be very grateful if someone had a better implementation to propose.


@Potatoswatter suggested using initializer lists, which helps a bit (no range-for there).

void printf_impl(char const*, std::initializer_list<Interface const*> array) {
    for (Interface const* e: list) { std::cout << *e; }
    std::cout << "\n";
}

template <typename... T>
void printf_bridge(char const* format, T const&... t) {
    printf_impl(format, {(&t)...});
}

But still does not solve the intermediate function issue.

like image 536
Matthieu M. Avatar asked Apr 15 '12 11:04

Matthieu M.


1 Answers

Making it lightweight hinges on eliminating the type parameterization. Your shim potentially instantiates something heavy-duty with the expression out << _t, so it might not really be a good example.

C varargs handles the problem by implicitly casting everything to intptr_t. If you only want to replicate C printf functionality, you can do the same with reinterpret_cast and an initializer_list.

template <typename... T>
void printf(char const* format, T const&... t) {
    printf_impl(format, { reinterpret_cast< std::intptr_t >( t ) ... } );
}

This is obviously suboptimal, but shims are inherently limited. You could do something else with polymorphic types in the initializer_list if you wanted.

In any case, this is exactly what initializer_list is meant for. It can only be constructed from a braced-init-list, making its size a compile-time constant. But the size can only be read back as a runtime constant. So its only practical use is to funnel templates differing only in list length to a common, variable-length implementation.

Add to that the lifetime semantics of initializer_list arguments — the objects are created in a contiguous array on the stack and die when the function call statement ends — and initializer_list looks a lot like <varargs>! (Edit: or your solution, which I have now actually gone back and read :vP )

Edit: Since containers can't directly store polymorphic objects, and smart pointers aren't appropriate for temporary argument objects, implementing polymorphism would require taking pointers to temporaries. Ugly, but legal due to the lifetime guaranteed for temporary objects:

template <typename... T>
void printf(char const* format, T const&... t) {
    printf_impl(format, std::initializer_list< Interface const * >
        { & static_cast< Interface const & >( shim(t) )... } );
}
like image 50
Potatoswatter Avatar answered Oct 24 '22 09:10

Potatoswatter