Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The variadic template constructor of my class cannot modify my class members, why is that so?

Tags:

I've been working on a task we got by our professor, where I have to work around a variadic template class. The problem is, I cannot modify the class members within the recursive constructor. I cannot figure out why this is the case, as soon as it goes into the next constructor call, it will discard my changes on the variable.

What I have tried:

  • using pointer int* count instead of int count

  • using a setter to set the counter

I have already googled around for hours, but cannot find a solving answer.

Header file "test.h":

#include <cstdarg>
#include <iostream>

class Counter {
private:
    int count = 0;
    int tmp;

public:
    template <typename... Rest> Counter (int t, Rest... rest) {
        count++;
        std::cout << "start recursive number " << count << "...\n";
        Counter(rest ...);
        tmp = t;
        std::cout << "end recursive number " << count << "...\n";
    }
    Counter (int t) {
        count++;
        tmp = t;
        std::cout << "reached end of recursive ->  " << count << "\n";
    }
};

main.cpp:

#include "test.h"
int main () {
    Counter a {0, 1, 2, 3, 4};
}

The output I got:

start recursive number 1...
start recursive number 1...
start recursive number 1...
start recursive number 1...
reached end of recursive ->  1
end recursive number 1...
end recursive number 1...
end recursive number 1...
end recursive number 1...
like image 672
PrettyCoffee Avatar asked Apr 13 '19 13:04

PrettyCoffee


2 Answers

Counter(rest ...); creates an unnamed temporary object, it does not recursively invoke constructor for this object. Each object is spawned with its own count therefore you get stream of 1 1 1 1

If you want to delegate object initialization to different constructor then it should be present in member initialization list. This does not seem like a good idea though:

template <typename... Rest> Counter (int t, Rest... rest)
:   Counter{rest...}
{
    count++;
    std::cout << "start recursive number " << count << "...\n";
    tmp = t;
    std::cout << "end recursive number " << count << "...\n";
}
like image 195
user7860670 Avatar answered Oct 31 '22 02:10

user7860670


As explained by VTT, calling Counter() inside the body of the constructor create a new Counter() object.

You can call, recursively, the contructors but you have to do this in the initialization list: look for "delegating contructors" for more informations.

I also would advise you against initialization (and modifications) of member object inside the body of contructors.

If your target is initialize count with the number of the arguments and tmp with the value of the last argument, I propose the following ("tag dispatching" based) solution

class Counter
 {
   private:
      struct tag
       { };

      int count = 0;
      int tmp;

      Counter (tag tg, std::size_t c0, int t) : count(c0), tmp{t}
       { std::cout << "end: " << tmp << ", " <<count << "\n"; }

      template <typename... Rest>
      Counter (tag t0, std::size_t c0, int t, Rest... rest) 
         : Counter{t0, c0, rest...} 
       { std::cout << "recursion: " << tmp << ", " << count << "\n"; }

   public:
      template <typename... Rest>
      Counter (Rest... rest) : Counter{tag{}, sizeof...(Rest), rest...} 
       { std::cout << "start: " << tmp << ", " << count << "\n"; }
 };

You can also avoid tag-dispatching and constructor recursion delegating the recursion about rest... to a method (maybe static and also constexpr, if you want) used to initialize tmp

class Counter
 {
   private:
      int count = 0;
      int tmp;

      static int getLastInt (int i)
       { return i; }

      template <typename ... Rest>
      static int getLastInt (int, Rest ... rs)
       { return getLastInt(rs...); }

   public:
      template <typename... Rest>
      Counter (Rest... rest)
         : count(sizeof...(Rest)), tmp{getLastInt(rest...)} 
       { std::cout << tmp << ", " << count << "\n"; }
 };

Off Topic: to be precise, your Counter class isn't "a variadic template class".

It's an ordinary (not template) class with one (two, in my first solution) variadic template constructor(s).

-- EDIT --

The OP asks

What if I need to get the count as a static const variable and an int array with the length of the counter within compile time and as class members? (Array will be filled with all constructor arguments) Is this within the C++ possibilitys?

A static const (maybe also constexpr) make sense only if the counter is a common value between all the instances of the class.

At the moment doesn't make sense because your Counter accept initialization lists of different length.

But suppose that number of the argument of the constructor is a template parameter (say N)... in that case count is simply N and can be static constexpr. You can define a std::array<int, N> for the values (also a int[N] but I suggest to avoid to use C-style arrays, when possible, and use std::array instead) and, making the constructor constexpr, you can impose the compile time initialization.

The following is a full compiling C++14 example (uses std::make_index_sequence and std::index_sequence that, unfortunately, are available only starting from C++14).

Observe that I've defined the f8 variable in main() as constexpr: only this way you can impose (pretending there isn't the as-is rule) that f8 is initialized compile-time

#include <array>
#include <iostream>
#include <type_traits>

template <typename T, std::size_t>
using getType = T;

template <std::size_t N, typename = std::make_index_sequence<N>>
struct foo;

template <std::size_t N, std::size_t ... Is>
struct foo<N, std::index_sequence<Is...>>
 {
   static_assert( sizeof...(Is), "!" );

   static constexpr auto count = N;

   const std::array<int, N> arr;

   constexpr foo (getType<int, Is> ... is) : arr {{ is ... }}
    { }
 };

int main ()
 {
   constexpr foo<8u>  f8 { 2, 3, 5, 7, 11, 13, 17, 19 };

   for ( auto const & i : f8.arr )
      std::cout << i << ' ';

   std::cout << std::endl;
 }

If you can use a C++17 enabled compiler, you can also use a deduction guide for foo

template <typename ... Args>
foo(Args...) -> foo<sizeof...(Args)>;

so there is no needs to explicate the template argument defining f8

// .......VVV  no more "<8u>"
constexpr foo  f3{ 2, 3, 5, 7, 11, 13, 17, 19 };

because it's deduced from the number of the argument of the constructor.

like image 40
max66 Avatar answered Oct 31 '22 03:10

max66