samedi 24 novembre 2018

How to instantiate the base class with the Curiously Recurring Template Pattern?

I defined a custom array implementation with template meta programming as follows:

#include <array>
#include <iostream>

template <unsigned int array_width, typename DataType, typename DerivedType>
struct Array {
  std::array<DataType, array_width> _data;

  Array() {
    for(int index = 0; index < array_width; ++index) _data[index] = 1;
  }

  DerivedType operator/(const double& data) {
    unsigned int column;
    DerivedType new_array;

    for(column = 0; column < array_width; column++) {
      new_array._data[column] = _data[column] / data;
    }
    return new_array;
  }

  friend std::ostream& operator<<( std::ostream &output, const Array &array ) {
    unsigned int column; output << "(";
    for( column=0; column < array_width; column++ ) {
      output << array._data[column];
      if( column != array_width-1 ) {
        output << ", ";
      }
    }
    output << ")"; return output;
  }
};

struct Coordinate : public Array<3, double, Coordinate> {
  typedef Array< 3, double, Coordinate > SuperClass;
  double& x;
  double& y;
  double& z;

  Coordinate() : SuperClass{}, x{this->_data[0]}, y{this->_data[1]}, z{this->_data[2]} {}
};

int main() {
  Coordinate coordinate;
  std::cout << "coordinate: " << coordinate << std::endl;

  Coordinate new_coordinate = coordinate / 10.0;
  std::cout << "new_coordinate: " << new_coordinate << std::endl;
}

But I cannot find a way to directly instantiate an array of the base class Array. For example, if I try the following:

int main() {
  Array<5, int> int_array;
  std::cout << "int_array: " << int_array << std::endl;

  Array<5, int> new_int_array = int_array / 10;
  std::cout << "new_int_array: " << new_int_array << std::endl;
}

The compiler says:

test.cpp: In function 'int main()':
test.cpp:45:15: error: wrong number of template arguments (2, should be 3)
   Array<5, int> int_array;
               ^
test.cpp:6:8: note: provided for 'template<unsigned int array_width, class DataType, class DerivedType> struct Array'
 struct Array {
        ^~~~~
test.cpp:48:15: error: wrong number of template arguments (2, should be 3)
   Array<5, int> new_int_array = int_array / 10;
               ^
test.cpp:6:8: note: provided for 'template<unsigned int array_width, class DataType, class DerivedType> struct Array'
 struct Array {
        ^~~~~

Then, I tried to pass the own template class as a default argument for the struct Array declaration as follows:

template <unsigned int array_width, typename DataType, typename DerivedType>
struct Array;

template <unsigned int array_width, typename DataType, typename DerivedType=Array>
struct Array {
  std::array<DataType, array_width> _data;
  // ...

However, I figured out the compiler seems to not allow to pass template classes to another template class, because they do not define a type if the are not instantiated yet.

test.cpp:8:77: error: invalid use of template-name 'Array' without an argument list
 template <unsigned int array_width, typename DataType, typename DerivedType=Array>
                                                                             ^~~~~
test.cpp:8:77: note: class template argument deduction is only available with -std=c++1z or -std=gnu++1z
test.cpp:6:8: note: 'template<unsigned int array_width, class DataType, class DerivedType> struct Array' declared here
 struct Array;
        ^~~~~
test.cpp: In function 'int main()':
test.cpp:48:15: error: template argument 3 is invalid
   Array<5, int> int_array;
               ^
test.cpp:51:15: error: template argument 3 is invalid
   Array<5, int> new_int_array = int_array / 10;

Hence, it seems a paradox because I cannot instantiate the myself with myself without knowing my complete definition beforehand. Then, I tried to create a dummy type called ConcreteArray as next:

struct ConcreteArray
{
};

template <unsigned int array_width, typename DataType, typename DerivedType=ConcreteArray>
struct Array {
  std::array<DataType, array_width> _data;
  // ...

However, this creates serious problems when directly instantiating the Array class, as the returned by the implemented quotient/division operator is not the correct instance required by the Curiously Recurring Template Pattern:

test.cpp: In function 'int main()':
test.cpp:52:43: error: conversion from 'ConcreteArray' to non-scalar type 'Array<5, int>' requested
   Array<5, int> new_int_array = int_array / 10;
                                 ~~~~~~~~~~^~~~
test.cpp: In instantiation of 'DerivedType Array<array_width, DataType, DerivedType>::operator/(const double&) [with unsigned int array_width = 5; DataType = int; DerivedType = ConcreteArray]':
test.cpp:52:45:   required from here
test.cpp:22:17: error: 'struct ConcreteArray' has no member named '_data'
       new_array._data[column] = _data[column] / data;
       ~~~~~~~~~~^~~~~

References:

  1. C++ static polymorphism (CRTP) and using typedefs from derived classes
  2. Curiously Recurring Template Pattern and statics in the base class
  3. Practical Uses for the "Curiously Recurring Template Pattern"

Aucun commentaire:

Enregistrer un commentaire