Include the Required Library Headers
You use both std::size_t
and EXIT_SUCCESS
in your program, so you should include <cstdlib>
. These identifiers might also be declared as a side-effect of another header file, but this is not portable.
Use constexpr
, not Macros
More precisely, use constexpr
when you can, and macros when you have to. Preprocessor macros only do lexical substitution, which can have many subtle errors and be extremely hard to debug. A constexpr
expression (or something less restrictive, like consteval
or inline
) gets you something type-safe and scoped that follows the syntax of the language.
Use Efficient Data Structures
You have a rectangular array here: every row is the same constant width. A vector
of vector
is an extremely inefficient data structure to represent this. In fact, it’s almost never the data structure you really want.
There are some other recommendations in other answers, but one very quick solution that duck-types perfectly to the code you’ve already written is:
static constexpr std::size_t D = 3;
using row=std::array< double, D >;
using point_matrix = std::vector<row>;
(If you genuinely do have a sparse matrix where being able to omit trailing empty columns matters, instead of fixed-width columns, the format you want is usually something like compressed sparse row.)
Let’s take a look at the generated assembly output for one of the inner loops, from Clang 15.0.0 with -std=c++20 -march=x86-64-v3 -O3
when we use a std::vector
of fixed-size arrays:
.LBB0_25: # =>This Inner Loop Header: Depth=1
vxorps ymm1, ymm0, ymmword ptr [rax + rdi]
vxorps ymm2, ymm0, ymmword ptr [rax + rdi + 32]
vxorps ymm3, ymm0, ymmword ptr [rax + rdi + 64]
vmovups ymmword ptr [rdx + rdi + 64], ymm3
vmovups ymmword ptr [rdx + rdi + 32], ymm2
vmovups ymmword ptr [rdx + rdi], ymm1
add rdi, 96
cmp r8, rdi
jne .LBB0_25
With dynamic vectors as rows:
- Each row must allocate its own dynamic memory.
- The
negative
object must initialize every row, move and delete them.
- The
negative_points
function must dereference every row and loop over it individually, with a cache miss each time.
- All lookups require double indirection.
With fixed-width arrays as rows:
- Allocating a new object requires a single call to the
new
handler, and with copy elision, delete
can be optimized away.
- The loop can be a single linear pass through the contiguous storage, with every row but the first preloaded.
- Accessing the storage requires a single dereference.
If the rows were larger, there would theoretically be one advantage to making the rows vectors
: they would then be moveable and swappable. With rows this small, the overhead of copying one is no greater than of moving a vector
.
Consider a More Flexible Approach
Since people here have suggested at least three different data structures, there’s a good chance you might want to change the data structure you use. I picked one that duck-typed to your existing code, but some of the other suggestions don’t.
In the abstract, what you want to do has a few different names in computer science, and one from functional programming is traverse
. That is, you want to take some kind of structure (or, more abstractly, a functor from category theory) that wraps elements of a type, here double
, pass it a function that takes that type as its input, and return a new structure with the same layout, but where all the data has been passed through the function.
Unfortunately, deducing the correct template parameters for arbitrarily-nested containers would be somewhere between very complicated and impossible, but you can at least generalize on the transformation function and overload the traversal. This lets you write:
inline point_matrix negative_points(const point_matrix &points) {
return traverse( points, std::negate<double>{} );
}
Putting it All Together
Here’s a version, based on this code by G. Sliepen, that should work for nested containers. It replaces my original implementation.
#include <algorithm> // transform
#include <array>
#include <cassert>
#include <concepts> // std::invocable
#include <cstdlib>
#include <fmt/ranges.h>
#include <functional> // invoke, negate
#include <iterator> // begin, end
#include <vector>
static constexpr std::size_t D = 3;
using row_t = std::array< double, D >;
using point_matrix = std::vector<row_t>;
template<class T>
concept is_iterable = requires(T x)
{
*std::begin(x);
std::end(x);
};
template<class T>
concept is_sized = requires(T x)
{
x.size();
};
template<class T>
concept is_reservable = requires( T x, std::size_t n )
{
x.reserve(n);
};
template<class T>
concept is_resizeable = requires( T x, std::size_t n )
{
x.resize(n);
};
template<class T, class U>
concept is_back_emplaceable = requires( T x, U y )
{
x.emplace_back(y);
};
template<class T, class U>
concept is_set_emplaceable = requires( T x, U y )
{
x.emplace_hint( std::begin(x), y );
};
template<class T, class U>
concept is_forward_list_emplaceable = requires( T x, U y )
{
x.emplace_after( std::begin(x), y );
};
template<class T, class U>
concept is_basic_emplaceable = requires( T x, U y )
{
x.emplace(y);
};
// Prototypes in case we ever want to nest in the opposite order.
template< template<typename...> typename Container,
typename F,
typename... Ts >
requires is_iterable<Container<Ts...>>
inline auto traverse ( const Container<Ts...>& input, const F& f );
template< template<class T, std::size_t> class Container,
typename F,
typename T,
std::size_t N >
requires is_iterable<Container<T, N>>
constexpr auto traverse( const Container<T, N>& input, const F& f );
template< typename T,
typename F,
std::size_t N >
constexpr auto traverse( const T(&input)[N], const F& f );
/* Base case for traversal: a single input to the transformation function.
*/
template< typename T,
std::invocable<T> F >
constexpr auto traverse( const T& input, const F& f )
{
return std::invoke( f, input );
}
/* This overload was needed to support traversing std::array.
*/
template< template<class T, std::size_t> class Container,
typename F,
typename T,
std::size_t N >
requires is_iterable<Container<T, N>>
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
using TransformedValueType = decltype(traverse(*std::begin(input), f));
Container<TransformedValueType, N> output;
std::transform( std::begin(input),
std::end(input),
std::begin(output),
[f](auto &x){ return traverse( x, f ); }
);
return output;
}
/* Since C++ does not allow functions to return built-in arrays, we transform
* them into std::array objects instead.
*/
template< typename T,
typename F,
std::size_t N >
constexpr auto traverse( const T(&input)[N], const F& f )
{
using TransformedValueType = decltype(traverse(*std::begin(input), f));
std::array< TransformedValueType, N > output;
std::transform( std::begin(input),
std::end(input),
std::begin(output),
[f](auto &x){ return traverse( x, f ); }
);
return output;
}
/* This implementation looks for the following member functions on the
* transformed container, in order: .emplace_back() (std::vector,
* std::dequeue, std::list), .emplace_after() (std::forward_list),
* emplace_hint (std::set, etc.), or .emplace(). These will fail on
* containers such as std::map, unless another overload enables traversal on
* key-value pairs. If none of those succeeds, it will attempt to resize the
* output container and call std::transform().
*/
template< template<typename...> typename Container,
typename F,
typename... Ts >
requires is_iterable<Container<Ts...>>
inline auto traverse ( const Container<Ts...>& input, const F& f )
{
using TransformedValueType = decltype(traverse(*std::begin(input), f));
using OutputC = Container<TransformedValueType>;
OutputC output;
// Should reserve storage if and only if the container supported.
if constexpr ( is_sized<Container<Ts...>> &&
is_reservable<Container<TransformedValueType>> ) {
output.reserve(input.size());
}
if constexpr (is_back_emplaceable<OutputC, TransformedValueType>) {
for( const auto& x : input ) {
output.emplace_back(traverse( x, f ));
}
} else if constexpr (is_forward_list_emplaceable<OutputC, TransformedValueType>) {
// This branch is NOT TESTED.
auto current = std::begin(output);
for( auto it = std::begin(input); it != std::end(input); ++it ) {
current = output.emplace_after( current, traverse( *it, f ) );
}
} else if constexpr (is_set_emplaceable<OutputC, TransformedValueType>) {
// This branch is NOT TESTED.
auto current = std::begin(output);
for( auto it = std::begin(input); it != std::end(input); ++it ) {
current = output.emplace_hint( current, traverse( *it, f ) );
}
} else if constexpr (is_basic_emplaceable<OutputC, TransformedValueType>) {
// This branch is NOT TESTED.
for ( const auto& x : input ) {
output.emplace(traverse( x, f ));
}
} else {
if constexpr ( is_sized<Container<Ts...>> &&
is_resizeable<OutputC>) {
output.resize(input.size());
}
std::transform( std::begin(input),
std::end(input),
std::begin(output),
[f](const auto& x){ return traverse( x, f ); }
);
}
// One last sanity check.
if constexpr( is_sized<Container<Ts...>> && is_sized<OutputC> ) {
assert( output.size() == input.size() );
}
return output;
}
auto negative_points(const point_matrix &points) {
return traverse( points, std::negate<double>{} );
}
int main() {
const point_matrix points{
{0, 0, 4},
{0, 5, 3},
{1, 7, 0},
{2, 1, 4},
{3, 4, 5},
{4, 2, 3},
{4, 4, 6},
{4, 6, 7},
{5, 0, 2},
{6, 4, 1},
{6, 5, 1},
{6, 7, 0},
{7, 4, 3}
};
fmt::print("Original: {}\n", points);
fmt::print("Negative: {}\n", negative_points(points));
return EXIT_SUCCESS;
}
In the code generated by Clang 15.0.0 with -std=c++20 -march=x86-64-v3 -O3
, there is one call to memmove
that might be optimized away, compared to just duck-typing your original code to the new types. But it’s still reasonably efficient, very general (and sadly overcomplicated).