3

I need to write a constexpr function that computes a determinant at compile time. The most obvious solution is to use Laplace expansion. C++14 is supported.

#include <array>
#include <utility>

constexpr int get_cofactor_coef(int i, int j) {
    return (i + j) % 2 == 0 ? 1 : -1;
}

template <int N>
constexpr int determinant(const std::array<std::array<int, N>, N>& a) {
    int det = 0;

    for (size_t i = 0u; i < N; ++i) {
        det += get_cofactor_coef(i, 1) * a[i][0] * determinant<N-1>(GET_SUBMATRIX_OF_A<N-1, I, J>(a);
    }

    return det;
}

template <>
constexpr int determinant<2>(const std::array<std::array<int, 2>, 2>& a) {
    return a[0][0] * a[1][1] - a[0][1] * a[1][0];
}

template <>
constexpr int determinant<1>(const std::array<std::array<int, 1>, 1>& a) {
    return a[0][0];
}

The problem is that I have absolutely no clue how to write the GET_SUBMATRIX_OF_A.

I know that I need to:

  1. Generate a sequence (using std::integer_sequence probably);
  2. Exclude from this sequence the i-th row;
  3. Copy all but the first (0-th) column;

My constexpr skills are next to non-existent. Head on attempts to pass a to another function result in weird errors like error: '* & a' is not a constant expression.

All help is greatly appreciated!

AVH
  • 11,349
  • 4
  • 34
  • 43
magom001
  • 608
  • 6
  • 16
  • Is using C++17 an option? Many more functions are constexpr in it, e.g. [array's `at()` function](https://en.cppreference.com/w/cpp/container/array/at), meaning you can almost write "standard" code and still have it be constexpr. – AVH Feb 27 '20 at 09:29
  • unfortunately no: I am told explicitly to use c++14 standard... – magom001 Feb 27 '20 at 09:31

2 Answers2

3

The problem is that the non-const std::array<T, N>::operator[] (returning T&) is not constexpr until C++17, making it difficult to set the elements of the minor.

However, there is an escape hatch, which is that std::get<I>(std::array&) is constexpr, and it is perfectly legitimate to perform pointer arithmetic on the result, so we can rewrite

a[i]  // constexpr since C++17

as

(&std::get<0>(a))[i]  // constexpr in C++14!!

That is, we use std::get to obtain a constexpr reference to the first member of the array, take a pointer to it, and use the built-in [] operator on the pointer and index.

Then a two-level array member access a[i][j] becomes the horrendously ugly but still constexpr (&std::get<0>((&std::get<0>(a))[i]))[j], meaning we can write get_submatrix_of_a as an ordinary constexpr function:

template<std::size_t N>
constexpr std::array<std::array<int, N - 1>, N - 1>
get_submatrix_of_a(const std::array<std::array<int, N>, N>& a, int i, int j) {
    std::array<std::array<int, N - 1>, N - 1> r{};
    for (int ii = 0; ii != N - 1; ++ii)
        for (int jj = 0; jj != N - 1; ++jj)
            (&std::get<0>(((&std::get<0>(r))[ii])))[jj] = a[ii + (ii >= i ? 1 : 0)][jj + (jj >= j ? 1 : 0)];
    return r;
}

Remember that const std::array<T, N>::operator[] is already constexpr in C++14, so we don't need to rewrite the RHS of the minor construction.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
0

Here's an example implementation. It might be possible to do this even shorter or more elegant, but it's a starting point. Actually, I just realized your matrices are square, so it's definitely possible to drop some template parameters in the code below.

As I mentioned in my comment, for C++17 and up, it's very likely none of this is required at all.

First, let's define some boilerplate that let's us create and index sequence with one value left out (i.e. the row you want to skip):

#include <utility>

// Based on https://stackoverflow.com/a/32223343.
template <size_t Offset, class T1, class T2>
struct offset_sequence_merger;

template <size_t Offset, size_t... I1, size_t... I2>
struct offset_sequence_merger<Offset, std::index_sequence<I1...>, std::index_sequence<I2...>>
  : std::index_sequence<I1..., (Offset + I2)...>
{ };

template <std::size_t Excluded, std::size_t End>
using make_excluded_index_sequence = offset_sequence_merger<Excluded + 1,
  std::make_index_sequence<Excluded>,
  std::make_index_sequence<End - Excluded - 1>>;

Now let's put this to use to extract sub-matrices:

#include <array>
template <class T, std::size_t N, std::size_t... Indices>
constexpr std::array<T, sizeof...(Indices)> extract_columns (
    std::array<T, N> const & source, std::index_sequence<Indices...>) {
  return { source.at(Indices)... };
}

template <class T, std::size_t N>
constexpr std::array<T, N - 1> drop_first_column (
    std::array<T, N> const & source) {
  return extract_columns(source, make_excluded_index_sequence<0, N>());
}

template <class T, std::size_t Rows, std::size_t Cols, std::size_t... RowIndices>
constexpr auto create_sub_matrix (
    std::array<std::array<T, Cols>, Rows> const & source,
    std::index_sequence<RowIndices...>) 
    -> std::array<std::array<T, Cols - 1>, sizeof...(RowIndices)> {
  return { drop_first_column(source.at(RowIndices))... };
}

template <std::size_t ExcludedRow, class T, std::size_t Rows, std::size_t Cols>
constexpr auto create_sub_matrix (
    std::array<std::array<T, Cols>, Rows> const & source)
    -> std::array<std::array<T, Cols - 1>, Rows - 1> {
  return create_sub_matrix(source,
    make_excluded_index_sequence<ExcludedRow, Rows>());
}

And finally, here's some code showing that the above seems to do what it should. You can see it in action at Wandbox:

#include <iostream>
#include <string>

template <class T>
void print_seq (std::integer_sequence<T> const & /* seq */) {
  std::cout << '\n';
}

template <class T, T Head, T... Tail>
void print_seq (std::integer_sequence<T, Head, Tail...> const & /* seq */) {
  std::cout << Head << ' ';
  print_seq(std::integer_sequence<T, Tail...>{});
}

template <class T, std::size_t N>
void print_array (std::array<T, N> const & src) {
  std::string sep = "";
  for (auto const & e : src) {
    std::cout << sep << e;
    sep = " ";
  }
  std::cout << '\n';
}

template <class T, std::size_t N, std::size_t M>
void print_matrix (std::array<std::array<T, N>, M> const & src) {
  for (auto const & row : src) { print_array(row); }
}

int main () {
  auto indexSeqA = make_excluded_index_sequence<0, 3>(); print_seq(indexSeqA);
  auto indexSeqB = make_excluded_index_sequence<1, 3>(); print_seq(indexSeqB);
  auto indexSeqC = make_excluded_index_sequence<2, 3>(); print_seq(indexSeqC);
  std::cout << '\n';

  std::array<int, 3> arr = { 1, 7, 9 };
  print_array(arr); std::cout << '\n';

  std::array<std::array<int, 3>, 3> matrix = {{
      { 0, 1, 2 }
    , { 3, 4, 5 }
    , { 6, 7, 8 }
  }};
  print_matrix(matrix); std::cout << '\n';

  print_matrix(create_sub_matrix<0>(matrix)); std::cout << '\n';
  print_matrix(create_sub_matrix<1>(matrix)); std::cout << '\n';
}

Hopefully that's enough to help you implement the determinant function completely. (P.S.: No need to explicitly provide the size_t template argument to determinant when calling it, it will be automatically deduced from the size of it's std::array argument).

AVH
  • 11,349
  • 4
  • 34
  • 43