Search code examples
c++parameter-passingc++17variadic

Variadic function template: automating N inputs at run time based on N compile time value


While working on one of my class members I ran into a stumbling block...

I'll briefly explain my data structure. I have two one dimensional vectors that are both indexed as two-d array structures. The data in my first table is organized as column major.

My class is a template that takes two integral type arguments. These are not the overall size of either table. The first argument is the number of inputs that are stored in the input table. The input table has a size of [N x 2^N] that is generated by the first template argument. The 2nd table is a [M x 2^N] where both [N] and [M] are the number of columns and [2^N] is the number of rows for both.

The purpose of the first table is to generate all of the possible values for a given N-Input truth table. For example if there are 3 inputs then the first table will have 3 columns and 8 rows. Then if M is 1 there will be 1 column with 8 rows, 2 columns and so on for the output table.

My data vector in memory looks like this:

Inputs: A, B, C; Outputs: X, Y

// input vector
{ a0 ... a7, b0 ... b7, c0 ... c7 }

// output vector
{ X0 ... X7, Y0 ... Y7 }       

The first table is automatically generated and this much I have completed. I have also completed the print out to these two tables in a side by side fashion where the inputs are to the left and the outputs to the right.

The set of functions are variadic templates as they can take any number of arguments. The variadic types here are homogeneous as they are all of the same type.

Within my class I'm store the function types as an enum class within a vector and I am using this within a switch statement to apply the appropriate function to the inputs per row and this is where I am a bit stuck...


Now for my question within my class's apply() function which you can see the full class below, I am able to easily enough index into the output table to set the desired output. I can easily enough calculate the initial index into the input table, but where I am stuck is how do I pass each of the N inputs within a given row as the arguments to the functions to be applied? All of the values are known at compile time, I just want to automate the passing of the inputs of a row as individual arguments into the output so for example consider the following truth table:

// Inputs: A, B  Outputs: Y, Y = And
0 0 | 0
0 1 | 0
1 0 | 0
1 1 | 1

// Intputs: A, B, C Outputs X, Y  X = Or Y = Xor
0 0 0 | 0 0
0 0 1 | 1 1
0 1 0 | 1 1
0 1 1 | 1 0
1 0 0 | 1 1
1 0 1 | 1 0
1 1 0 | 1 0
1 1 1 | 1 (0 or 1) // depending on interpretation of XOr: Single bit high=true or odd parity=true
// Here I'm using C++ operator ^ as the default intepretation!

So as you can see above one can instantiate this class template as seen above: BinaryTTGenerator<2,1> and BinaryTTGenerator<3,2> respectively.

I just need to know how I would be able to apply 2 inputs for the first, and 3 inputs for the second where the amount of inputs to be passed to the respective function is defined by N. I am open to any suggestions and possibilities if it can be done!

Here is my apply() function from my class below:

void apply() {
    for (u16 f = 0; f < M; ++f) {
        for (u16 y = 0; y < numRows_; ++y) {
            for (u16 x = 0; x < N; ++x) {
                u16 index = y * M + x - N;
                switch (functionTypes_[f]) {
                case BFT::AND:
                    outputTable_[f] = And(inputTable_[index], ... ?); break;
                case BFT::OR:
                    outputTable_[f] = Or(inputTable_[index], ... ?); break;
                case BFT::NAND:
                    outputTable_[f] = Nand(inputTable_[index],... ?); break;
                case BFT::NOR:
                    outputTable_[f] = Nor(inputTable_[index], ... ?); break;
                case BFT::XOR:
                    outputTable_[f] = Xor(inputTable_[index], ... ?); break;
                case BFT::XNOR:
                    outputTable_[f] = XNor(inputTable_[index], ... ?); break;
                default:
                    std::cout << "Invalid Logic function applied to this template\n";
                }
            }
        }
    }       
}

Also, I'm not sure if the double loop needs to be outside of the switch or performed within each case statement...


Here is my current class so far:

#pragma once

// Binary Truth Table Generator

#include <algorithm>
#include <array>
#include <bitset>
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <iostream>
#include <sstream>
#include <string>
#include <type_traits>
#include <vector>

using u16 = std::uint16_t;
using Bit = std::bitset<1>;

// The Boolean Operational Functions:
enum class BFT {        
    BUFFER, // Not included within the switch statement of the class!
    NOT,    // Both Not and And are non variadic but can be applied
            // directly to a specific input, or to another function
            // as these both take in a `Bit` and return a `Bit` type.

    AND,   // The following types are all variadic as they can 
    OR,    // have any number of arguments.
    NAND,
    NOR,
    XOR,
    XNOR

    // Possible Future Implementations:
    // Tristate Buffer and Tristate Controlled Buffer.
};

// Helper Templates
template <typename... Bits>
constexpr bool all_bits() {
    return (std::is_same_v<Bits, Bit> && ...);
}

template <typename... FuncTypes>
constexpr bool all_same() {
    return (std::is_same_v<FuncTypes, BFT> &&...);
}

// Unary Functions
auto Buffer(Bit& b) -> auto {
    return b;
}    
auto Not(Bit& b) -> auto {
    return ~b;
}

// Binary Functions with multiple inputs.
template<typename... Bits>
std::enable_if_t<all_bits<Bits...>(), Bit>
 And(Bits... bits) {
    return (bits&...);
}
template<typename... Bits>
std::enable_if_t<all_bits<Bits...>(), Bit>
Or(Bits... bits) {
    return (bits|...);
}

template<typename... Bits>
std::enable_if_t<all_bits<Bits...>(), Bit>
Nand(Bits... bits) {
    return ~(bits&...);
}

template<typename... Bits>
std::enable_if_t<all_bits<Bits...>(), Bit>
Nor(Bits... bits) {
    return ~(bits|...);
}

template<typename... Bits>
std::enable_if_t<all_bits<Bits...>(), Bit>
Xor(Bits... bits) {
    return (bits^...);
}

template<typename... Bits>
std::enable_if_t<all_bits<Bits...>(), Bit>
XNor(Bits... bits) {
    return ~(bits^...);
}

// N is the number of inputs where M is the number of functions performed on the set or row of N
template<u16 N, u16 M>
struct BinaryTTGenerator {
    // Calculate the Number of Cols & Rows as well
    // as the stride for indexing into the vector
    // typically the stride should almost always
    // equal that of the number of rows.
    const u16 numCols_ = M + N;
    const u16 numRows_ = 1U << N;
    const u16 stride_ = numCols_;

    // Calculate the grid sizes there are 2 different grids
    // as well as the overall grid, which are loosely combined.
    // The first grid is all of the possible combinations
    // of the inputs, the second grid is the respective outputs
    // to each applied function to the set of inputs on a specific
    // row. The combined grid or table is that concatenation of the two
    // with the input grid on the left and the output grid on the right.
    const u16 inputGridSize_ = N * numRows_;
    const u16 outputGridSize_ = M * numRows_;

    std::vector<Bit> inputTable_ = std::vector<Bit>(inputGridSize_, Bit{ 0 });
    std::vector<Bit> outputTable_ = std::vector<Bit>(outputGridSize_, Bit{ 0 });

    std::vector<BFT> functionTypes_;

    BinaryTTGenerator() = default;
    explicit BinaryTTGenerator(BFT bft) : functionTypes_{ bft } {}

    template<typename... FuncTypes>
    BinaryTTGenerator(FuncTypes... funcs) {
        /*static_assert((sizeof...(funcs) + 1) == M, "Aguments does not equal the number of functions");
        static_assert(std::is_same<
            std::integer_sequence<bool, true, std::is_same<BFT, std::remove_reference_t<First>>::value>,
            std::integer_sequence<bool, std::is_same<BFT, std::remove_reference_t<First>>::value, true >
        > ::value, "!");
        static_assert(std::is_same<
            std::integer_sequence<bool, true, (std::is_same<BFT, std::remove_reference_t<FuncTypes>>::value)...>,
            std::integer_sequence<bool, (std::is_same<BFT, std::remove_reference_t<FuncTypes>>::value)..., true>
        >::value, "!");*/
        functionTypes_{ funcs... };
    }

    // initializes all of the input values 
    void initialize() {
        u16 fill = 1U << (N - 1);
        for (u16 col = 0; col < N; ++col, fill >>= 1U) {
            for (u16 row = fill; row < (1U << N); row += (fill * 2)) {
                u16 index = col*numRows_ + row;
                std::fill_n(&inputTable_[index], fill, 1);
            };
        }
    }

    // apply the set of M functions individually on the N(row) of inputs.
    void apply() {
        for (u16 f = 0; f < M; ++f) {
            for (u16 y = 0; y < numRows_; ++y) {
                for (u16 x = 0; x < N; ++x) {
                    u16 index = y * M + x - N;
                    switch (functionTypes_[f]) {
                    case BFT::AND:
                        outputTable_[f] = And(inputTable_[index]); break;
                    case BFT::OR:
                        outputTable_[f] = Or(inputTable_[index]); break;
                    case BFT::NAND:
                        outputTable_[f] = Nand(inputTable_[index]); break;
                    case BFT::NOR:
                        outputTable_[f] = Nor(inputTable_[index]); break;
                    case BFT::XOR:
                        outputTable_[f] = Xor(inputTable_[index]); break;
                    case BFT::XNOR:
                        outputTable_[f] = XNor(inputTable_[index]); break;
                    default:
                        std::cout << "Invalid Logic function applied to this template\n";
                    }
                }
            }
        }       
    }

    void show() {
        for (u16 y = 0; y < numRows_; ++y) {  // y - height
            for (u16 x = 0; x < numCols_; ++x) { // x - width

                if (x < N) {
                    // The index variables are not necessary - I don't mind the extra variable.
                    // I'm using it for readability that pertains to the index value of a container. 
                    // It is also easier to adjust or fix the equation to calculate the appropriate
                    // index value into the desired container. 
                    std::size_t index = x * numRows_ + y;
                    std::cout << inputTable_[index].to_string() << " ";
                } else {
                    std::size_t index = y * M + x - N;
                    std::cout << outputTable_[index].to_string() << " ";
                }
            }
            std::cout << '\n';
        }
    }

};

Solution

  • Something along these lines (not tested):

    template <std::size_t... I>
    void apply_impl(std::index_sequence<I...>) {
    // ...
      case BFT::AND:
        outputTable_[f] = And(inputTable_[index + I]...); break;
    // ...
    }
    
    void apply() {
      return apply_impl(std::make_index_sequence<N>());
    }