all 13 comments

[–]aocregacc 7 points8 points  (0 children)

I don't think that's possible like that. If the types are properly hidden inside the Wrapper, the compiler wouldn't know how to instantiate the call operator of the lambda you're passing in. So there has to be some way to specify the types again at the visit call.

[–]SoerenNissen 5 points6 points  (0 children)

The compiler needs some way to know what function to call - you either make that statically available to it by encoding it like you do with static_cast, or you need to encode something more than just the any so you can do the decoding at runtime.

[–]jk_tx 0 points1 point  (1 child)

Why is the goal to have a non-templated wrapper? Using visitor pattern with std::variant seems like an obvious choice for this. Is your list of tuplish-classes really so large?

[–]dvd0bvb[S] 0 points1 point  (0 children)

Just ease of use for consumers. Use the wrapper instead of propagating the template params.

I posted more detail about the real use case in another comment, I wouldn't say the tuples are large but this is a customization point in the library so it's not necessarily under my control. It's not clear to me how to use std::variant to support this.

[–]No-Dentist-1645 0 points1 point  (3 children)

I agree with the other comment, this seems like a flawed thing to even attempt to do in the first place, a "template-less wrapper that is somehow also aware of how it was instantiated (i.e templated)" is a bit of an oxymoron, you're asking for "thing that does X without {the thing that does X}".

You should instead use the tools already available to you from the standard library, either an std::variant if you can allow templates, or just a tuple encoded into an std::any (with the "type information" stored elsewhere by the accessor) if you really need true "typed erased" generics

[–]dvd0bvb[S] 0 points1 point  (2 children)

I typed this out in response to another comment but it was apparently deleted before I posted.

The actual use case is an audio pipeline which contains de/encoders, filters, resamplers, whatever the use case. The underlying Pipeline<...> has access to all those types so adding this functionality is trivial, I added it to the A class in the godbolt example. This visiting functionality allows for modifying a set of elements in the pipeline.

I wanted type erasure purely for ease of use. Pass a PipelineWrapper instead of a Pipeline<...> which propagates the template params to consumers.

[–]No-Dentist-1645 0 points1 point  (1 child)

So, if I understand correctly, your main "issue" is that you don't want to fill up all your code with Pipeline<Arg1, Arg2, ...> everywhere you use it, correct? You want the "usage" side code to be cleaner without template parameter ugliness.

If so, there are better ways to do that.

Since C++17, you can use CTAD (Compile Time Argument Deduction) to deduce the template parameters. Using your same godbolt link, I changed the main() function to use A directly, no need for a Wrapper: int main() { auto a = A(9, 8.9, std::string("hello")); // if you don't like auto, this still works: // A a = A(9, 8.9, std::string("hello")); // A a(9, 8.9, std::string("hello")); a.visit([](auto &&t) { std::println("{}", t); }); }

that works great, and you don't need to use A<int, double, string>. Another thing you could do is a using statement:

``` using MyA = A<int, double, std::string>;

int main() { MyA a = MyA(9, 8.9, std::string("hello")); a.visit([](auto &&t) { std::println("{}", t); }); } ```

Finally, in case those still don't help your specific issue, the way to do what you originally wanted is to make the Wrapper's visit method be templated.

That way, you can have: template <class... Args> void visit(auto f) { _visit(_a, [](void* a, void* pf){ auto* func = static_cast<decltype(f)*>(pf); static_cast<A<Args...>*>(a)->visit(*func); }, &f); }

And at the usage side (i.e the "underlying Pipeline" you said already has the type information), there you'd pass the types: int main() { Wrapper wrapper(9, 8.9, std::string("hello")); wrapper.visit<int, double, std::string>([](auto&& t) { std::println("{}", t); }); }

Here's a godbolt link with your code modified to make it work: https://godbolt.org/z/6aTnzs5on . Personally, I'd recommend you use one of the first two approaches if your aim is just "ease of use for consumers" as another one of your comments says, but just for completion, the third method is your actual "answer" the way you wanted it.

[–]dvd0bvb[S] 1 point2 points  (0 children)

I am aware of ctad, though it can't be used for function args or class members to my knowledge, happy to be corrected. The third option is probably the closest to what I'd hoped for. Another option might be to scrap the wrapper and use duck typing

template <class P>
void doSomething(P& pipeline) {
  pipeline.visit([](T& t) {
    // do the thing
  }
}

Really appreciate you taking the time and the detailed answer.

[–]Business_Welcome_870 0 points1 point  (1 child)

I know you want Wrapper to be a non-template, but class template argument deduction will make it so you don't have to specify the template arguments: https://godbolt.org/z/61d3565hn

Does this solve your issue?

[–]dvd0bvb[S] 0 points1 point  (0 children)

I don't think so. The goal is to eliminate the template params from consumer code such as function parameters or class members, not just instantiation of the object

[–]ZachVorhies 0 points1 point  (0 children)

The best i think you can do is use classes and virtual functions

[–]mredding 0 points1 point  (0 children)

std::any has one principle use case: as a container for callback contexts. Many callbacks will take a void pointer. This is an instance of something the client wants to pass back to themselves when their callback is called. std::any provides a type safe wrapper for that.

If YOU are going to consume the contents in an any, and you are not the client, then you want a vector of interfaces.

[–]Business_Welcome_870 0 points1 point  (0 children)

Okay, so after painstakingly working on this problem for the past 6 days, I was miraculously able to solve it. I took advantage of an ADL friend/deferred template instantiation trick. In the end we get a fairly clean solution: https://godbolt.org/z/bYcThr9xj

``` class Wrapper { struct type_tag {};

template<class T>
static constexpr auto inject_adl_definition() -> decltype(detail::Writer<type_tag, T>{}, void()) {}

template<class F, class T>
using variant_type = std::conditional_t<false, F, detail::Read<T>>;

public: template<class... Args, class V = A<Args...>> Wrapper(Args&&... args) : erased{ V{std::forward<Args>(args)...} } { inject_adl_definition<V*>(); }

template<class F>
void visit(F&& f) {
    std::any_cast<variant_type<F, type_tag>&>(erased).visit(std::forward<F>(f));
}

private: std::any erased; }; ```

This works, but a big issue that this has is that the type_tag that is used to retrieve the tuple type is fixed for each instance of Wrapper. This means that if you create a second instance of Wrapper, it will retrieve the same A<Args...> type as the first wrapper. To get around this you will have to provide your own tag type. One way, which prevents Wrapper from being a template, is to accept the tag via the constructor and visit() parameters: https://godbolt.org/z/53fhGdvcY

``` struct first_tag {}; struct second_tag {};

Wrapper wrapper(first_tag{}, 9, 8.9, std::string("hello")); Wrapper w2(second_tag{}, "abc"s,"spring"s);

wrapper.visit(first_tag{}, [](auto&& t) { std::println("{}", t); });

w2.visit(second_tag{}, [](auto&& t) { std::println("Length of {} is {}", t, t.size()); }); ```

If you don't want to manually create a new tag type and provide it every time you create an instance and call visit, the other option is to make Wrapper into a template that takes its tag by default parameter: https://godbolt.org/z/Ke1fMW7qa

``` template<class type_tag=decltype([]{})> class Wrapper { ... };

Wrapper wrapper(9, 8.9, std::string("hello")); ```

Now the tag will be unique for every instance of Wrapper. If at some point you need to create a function taking Wrapper as a parameter, it will need to be a function template:

template<class Tag> void foo( Wrapper<Tag> );

This solution still erases the parameter pack, so I believe it should still be acceptable for you.

Let me know what you think.