Search code examples
c++templatesc++14sfinaevariadic

Parameter pack with pervious parameter type match


So Im using a simple example to try to understand variadic templates and some tmp techniques. The example consists on a Timer class which has a toc() method. The toc methods its used to stop the timer and call a function which decides what to do (print it, save it in a variable...)

So I have coded this idea like this (ive removed the timing bits)

class VerbosePolicy {
public:
    VerbosePolicy() {}

    explicit VerbosePolicy(const std::string &message) : m_message(message) {}

    VerbosePolicy(VerbosePolicy &&other) { m_message = other.m_message; }

    void operator()(double time) { std::cout << m_message << time << std::endl; }

private:
    std::string m_message;
};

template <typename Policy, typename... Args> class Timer {
public:
    Timer(Args... args) : m_policy(Policy(std::forward<Args>(args)...)) {}

    void toc(double time) { m_policy(time); }

private:
    Policy m_policy;
};

Here I create a Timer with a Policy and call the ctor of the Policy with the parameter pack. This way I can control how the Policy works (for example, I could pass a variable and store there the result).

Now, I want to use this

int main(int argc, char **argv) {
    std::string string = "Elapsed time";
    Timer<VerbosePolicy> timer(string);
    timer.toc(1.0);
}

The problem here is that the compiler is not able to determine that the string is part of the parameter pack, and its trying to match it to the Policy time, which fails.

I have tried adding a default argument for the Timer ctor

Timer(Args... args, Policy policy = Policy())

But this also fails, as its still trying to match de string to the policy type (in this case, it tries to call the second ctor, which fails because its marked as explicit. If I remove it, it compiles, but works wrong because the policy value its incorrect).

Everything works fine if I write

Timer<VerbosePolicy, std::string> timer(string)

as it doesn't need to deduce the variadic template anymore.

Is there anyway I can avoid writing the std::string? Thanks!

EDIT:

So to be complete and address some of the problems talked in the comments of the valid answer, I have been trying to deactivate the variadic constructor when the parameter its of the same type as the Timer, without success.

My approach has been

template <typename T, typename... Tail> struct first_of { using type = T; };

template <typename Policy> class Timer {
public:
  template <
      typename... CArgs,
      std::enable_if_t<!std::is_same<Timer<Policy>,
                                     typename first_of<CArgs...>::type>::value,
                       int> = 0>
  Timer(CArgs &&... args) : m_policy(std::forward<CArgs>(args)...) {}

  Timer(const Timer<Policy> &other) : m_policy(other.m_policy) {}

  void toc(double time) { m_policy(time); }

private:
  Policy m_policy;
};

int main(int argc, char **argv) {
  std::string string = "Elapsed time";
  Timer<VerbosePolicy> timer(string);
  Timer<VerbosePolicy> timer2(timer);
  timer.toc(1.0);
}

But the compiler still tries to use the variadic constructor for timer2. Im not sure why its trying to do this, since the two types passed to std::is_same should be equal, and therefore the ctor should be deactivated.

What am I misunderstanding?

Thanks again!


Solution

  • Did you try to make your constructor template instead?

    Like :

    template <typename Policy> class Timer {
    public:
        template<typename ...Args>
        Timer(Args && ... args) : m_policy(std::forward<Args>(args)...) {}
    
        void toc(double time) { m_policy(time); }
    
    private:
        Policy m_policy;
    };
    

    Btw, you are using std::forward in the wrong way. What you are doing is that :

    template<typename T>
    void foo(T v) {
        std::forward<T>(v);
    }
    

    In such code, T is a non reference value. So forward it here means : T&& so it is the same as "move"

    if you want to forward reference, you must use forwarding reference :

    template<typename T>
    void foo(T &&v) {
        std::forward<T>(v);
    }
    

    If the argument is a lvalue reference, here T is a T& and if the argument is a rvalue reference, T is a T and by forwarding reference v is respectively a T& &&so a T& and T && so a T&& ;)

    EDIT : As said in commentary, this code does not work when you give a Timer to the constructor. There is some way to avoid this issue, SFINAE can help you for example ;)

    EDIT 2 : You want to keep a track of your Args ... as you said in commentary.

    Let's say you have a class like that :

    template<typename ...Args>
    class Foo {
    public:
        Foo(Args... args) : noexcept m_tuple{std::move(args)...} {}
    private:
        std::tuple<Args...> m_tuple;
    };
    

    You want to deduce type : there is two ways :

    1) Before C++ 17 :

    template<typename ...Args>
    Foo<Args...> make_foo(Args ...args) {
        return {args...};
    }
    
    auto d = make_foo(5, 3.0); // Foo<int, double>
    

    2) After c++ 17

    template<typename ...Args>
    Foo(Args...) -> Foo<Args...>;
    
    Foo foo{3.0, "Lol"s}; // Foo<double, std::string>
    

    The name for this is deduction guide.