Abusing Operator Comma
07 Jul 2019First off, I’d like to preface this discussion by saying DO NOT DO THIS. There is currently a proposal to deprecate the use of the comma operator in subscripting expressions in order to allow for proper multidimensional overloads. This would allow us to write array2d[0, 0]
rather than needing to rely on undesirable type nesting (for array[0][0]
) or “abusing” the function call operator the way Eigen does array2d(0, 0)
. In section 4, the proposal notably mentions the parsing library Boost Spirit Classic (deprecated since 2009) as one of the few real-world users of this comma abuse technique. Now, with that out of the way:
How many times are you working with your home-grown 2D matrix class and you accidentally mixed up indexing by X/Y vs indexing by Row/Col? Just a few short weeks ago I stumbled on this when rigging up a quick Tetris game. Indexing std::array<std::array<T, COLS>,ROWS> arr
with arr[x][y]
isn’t just incorrect, it’s embarrassing. Now, I of course read paragraph 1 of this article, so I didn’t actually do this, but I did wonder if there was a way to leverage C++’s type-safe mechanisms to prevent me from making this mistake at compile time… (it’s C++, so the answer is obviously “yes, with some effort.”)
Using the power of user defined literals and overloading the comma operator, we can simulate multi-dimensional subscript operators, and overload on types! We simply make types for Coordinates in forms of X
, Y
, Row
, and Column
. Next, create our user defined literals for convenience. Finally, create our comma operators and leverage a common MatrixIndex
type, which interprets the coordinates in a common form.
#include <array>
#include <iostream>
struct CoordinateX { size_t value; };
constexpr CoordinateX operator""_x(size_t x) { return { x }; }
struct CoordinateY { size_t value; };
constexpr CoordinateY operator""_y(size_t y) { return { y }; }
struct Col { size_t value; };
constexpr Col operator""_col(size_t col) { return { col }; }
struct Row { size_t value; };
constexpr Row operator""_row(size_t row) { return { row }; }
struct MatrixIndex {
size_t row;
size_t col;
};
constexpr MatrixIndex operator,(CoordinateX x, CoordinateY y) {
return { y.value, x.value };
}
constexpr MatrixIndex operator,(Row row, Col col) {
return { row.value, col.value };
}
template <typename T, size_t ROWS, size_t COLS>
class Matrix {
public:
constexpr T& operator[](MatrixIndex idx) { return _matrix[idx.row][idx.col]; }
private:
std::array<std::array<T, COLS>, ROWS> _matrix;
};
int main()
{
Matrix<int, 3, 2> matrix{};
matrix[0_row, 0_col] = 1;
matrix[1_row, 0_col] = 2;
matrix[2_row, 0_col] = 3;
matrix[1_x, 0_y] = 9;
matrix[1_x, 1_y] = 8;
matrix[1_x, 2_y] = 7;
for (size_t row = 0; row < 3; ++row) {
std::cout << "[ ";
for (size_t col = 0; col < 2; ++col) {
std::cout << matrix[Row{ row }, Col{ col }] << ", ";
}
std::cout << "]\n";
}
return 0;
}
And we get the following, just as we’d expect:
[ 1, 9, ]
[ 2, 8, ]
[ 3, 7, ]
For bonus points, compile with C++17 to get constexpr
subscript operators on std::array
. And vuala, you now have the power to perturb friends and coworkers alike.