Search code examples
c++variadic-templatesvariadic-functions

How to overwrite operator in C++ class with a variadic function?


C++ newbie here: I want to create a template class to create tensors of different data types and d dimensions, where d is specified by a shape. For example, a tensor with shape (2, 3, 5) has 3 dimensions holding 24 elements. I store all data elements using a 1d vector and want to access elements using the shape information to find the elements.

I would like to overwrite the () operator to access the elements. Since the dimensions can vary, so can the number of input parameters for the () operator. Technically, I can use a vector as input parameter but C++ also seems to support variadic functions. However, I cannot wrap my head around it.

What I have so far:

#ifndef TENSOR_HPP
#define TENSOR_HPP

#include <vector>
#include <numeric>
#include <algorithm>
#include <stdexcept>
#include <iostream>
#include <stdarg.h>


template <typename T> class Tensor {

    private:
        std::vector<T> m_data;
        std::vector<std::size_t> m_shape;
        std::size_t m_size;
        
    public:
        // Constructors
        Tensor(std::vector<T> data, std::vector<std::size_t> shape);

        // Destructor
        ~Tensor();

        // Access the individual elements                                                                                                                                                                                               
        T& operator()(std::size_t&... d_args);
        
};


template <typename T> Tensor<T>::Tensor(std::vector<T> data, std::vector<std::size_t> shape) {
    // Calculate number of data values based on shape
    m_size = std::accumulate(std::begin(shape), std::end(shape), 1, std::multiplies<std::size_t>());
    // Check if calculated number of values match the actual number
    if (data.size() != m_size) {
        throw std::length_error("Tensor shape does not match the number of data values");
    } 
    // All good from here
    m_data = data;
    m_shape = shape;
}

template <typename T> T& Tensor<T>::operator() (std::size_t&... d_args) {
    // Return something to avoid warning
    return m_data[0];
};

template <typename T> Tensor<T>::~Tensor() {
    //delete[] m_values;
};


#endif

No when I do the following:

std::vector<float> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24};
std::vector<std::size_t> shape = {2, 3, 4};
Tensor<float> tensor(data, shape);

tensor(2,0,3); // <-- What I would like to do

// Possible workaround with vector which I would like to avoid
// std::vector<std::size_t> index = {2,0,3};
// tensor(index);

I get the error:

tensor2.hpp:27:33: error: expansion pattern ‘std::size_t&’ {aka ‘long unsigned int&’} contains no parameter packs

What is the correct approach to overwrite the () operator using a variadic function?


Solution

  • By providing "shape" as template parameter, you might do:

    // Helper for folding to specific type
    template <std::size_t, typename T> using always_type = T;
    
    // Your Tensor class
    template <typename T, std::size_t... Dims>
    class MultiArray
    {
    public:
    
        explicit MultiArray(std::vector<T> data) : values(std::move(data))
        {
            assert(values.size() == (1 * ... * Dims));
        }
    
        const T& get(const std::array<std::size_t, sizeof...(Dims)>& indexes) const
        {
            return values[computeIndex(indexes)];
        }
        T& get(const std::array<std::size_t, sizeof...(Dims)>& indexes)
        {
            return values[computeIndex(indexes)];
        }
    
        const T& get(always_type<Dims, std::size_t>... indexes) const
        {
            return get({{indexes...}});
        }
        T& get(always_type<Dims, std::size_t>... indexes)
        {
            return get({{indexes...}});
        }
    
        static std::size_t computeIndex(const std::array<std::size_t, sizeof...(Dims)>& indexes)
        {
            constexpr std::array<std::size_t, sizeof...(Dims)> dimensions{{Dims...}};
            size_t index = 0;
            size_t mul = 1;
    
            for (size_t i = dimensions.size(); i != 0; --i) {
                assert(indexes[i - 1] < dimensions[i - 1]);
                index += indexes[i - 1] * mul;
                mul *= dimensions[i - 1];
            }
            assert(index < (1 * ... * Dims));
            return index;
        }
    
        static std::array<std::size_t, sizeof...(Dims)> computeIndexes(std::size_t index)
        {
            assert(index < (1 * ... * Dims));
    
            constexpr std::array<std::size_t, sizeof...(Dims)> dimensions{{Dims...}};
            std::array<std::size_t, sizeof...(Dims)> res;
    
            std::size_t mul = (1 * ... * Dims);
            for (std::size_t i = 0; i != dimensions.size(); ++i) {
                mul /= dimensions[i];
                res[i] = index / mul;
                assert(res[i] < dimensions[i]);
                index -= res[i] * mul;
            }
            return res;
        }
    
    private:
        std::vector<T> values; // possibly: std::array<T, (1 * ... * Dims)>
    };
    

    Usage would be similar to

    std::vector<float> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24};
    MultiArray<float, 2, 3, 4> tensor(data);
    std::cout << tensor.get(1, 0, 3); // 16
    

    Demo