Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tag dispatching, variadic template, universal reference and the missed const specifier

Please, consider the following example (tag dispatching, variadic template, perfect forwarding, and so on, all in one):

#include <iostream>
#include <utility>
#include <string>

struct A { };
struct B { };

void doIt(A&&, const std::string &) {
    std::cout << "A-spec" << std::endl;
}

template<typename T, typename... Args>
void doIt(T&&, Args&&...) {
    std::cout << "template" << std::endl;
}

template<typename T, typename... Args>
void fn(Args&&... args) {
    doIt(T{}, std::forward<Args>(args)...);
}

int main() {
    const std::string foo = "foo";
    std::string bar = "bar";
    fn<A>(foo);
    fn<A>(bar);
    fn<B>(foo);
}

In that case, the output is:

A-spec
template
template

The reason is quite obvious, it doesn't bother me.

What I'd like to achieve is to call the first instance of the doIt function in both the cases, no matter if the string has the const specifier or not.
Of course, a possible solution is to define a new doIt with the right prototype, anyway I'd like to know if there is also another solution.

So far, I've tried to get it by means of add_const, but I'm quite sure I missed something.
Is there any viable solution to add silently the const specifier and get it working?

EDIT

I've updated the example above, in order to be more coherent with the real problem.
Moreover, despite the interesting answers, I forgot to cite that this is only a reduced example, so the real problem does not involve only std::string. Instead, it could happen that (as an example) for tag A the arguments are int and const std::string &, while for tag B the arguments are float, instance of class C, and so on.
Because of that, those answers that try to solve the problem using somehow the std::string type won't solve the real problem, I'm sorry.

like image 261
skypjack Avatar asked Dec 25 '22 10:12

skypjack


2 Answers

Introduce two separate functions, so that they won't collide with each other and the compiler won't raise any ambiguity error:

void doIt(A&&, const std::string &)
{
    std::cout << "A-spec" << std::endl;
}

template <typename T, typename... Args>
void doIt_template(T&&, Args&&...)
{
    std::cout << "template" << std::endl;
}

Prioritize two additional overloads; the preferred one is that which tries to call a specialized, non-templated version of a target function:

template <typename T, typename... Args>
auto fn_impl(int, Args&&... args)
    -> decltype(doIt(T{}, std::forward<Args>(args)...), void())
{
    doIt(T{}, std::forward<Args>(args)...);
}

template <typename T, typename... Args>
void fn_impl(char, Args&&... args)
{
    doIt_template(T{}, std::forward<Args>(args)...);
}

Introduce a single, general dispatcher:

template <typename T, typename... Args>
void fn(Args&&... args)
{
    fn_impl<T>(0, std::forward<Args>(args)...);
}

DEMO

like image 128
Piotr Skotnicki Avatar answered Jan 26 '23 00:01

Piotr Skotnicki


Overloading on forwarding references is a pain, as they are very, very greedy.

One option would be to disable the template version if Args... is a single argument convertible to std::string:

template<typename... Args>
struct convertible_to_single_string {
    static std::true_type help (std::string);
    static std::false_type help (...);
    using type = decltype(help(std::declval<Args>()...));
    static constexpr auto value = type::value;
};

template<typename... Args>
std::enable_if_t<!convertible_to_single_string<Args...>::value, void> 
doIt(int, Args&&...) {
    std::cout << "template" << std::endl;
}

This would also call the string version for const char* or anything with a user-defined conversion to std::string. If you don't want that, there are other possibilities, like removing all references and cv-qualifiers from the type and checking if it std::is_same<std::string, T>.

Live Demo

like image 32
TartanLlama Avatar answered Jan 25 '23 22:01

TartanLlama