Home Ā» C++23: More small pearls in the core language

C++23: More small pearls in the core language

by admin
C++23: More small pearls in the core language

C++23: More small pearls in the core language

The core language C++23 has more to offer than the great innovation Deducing This. The smaller but exciting innovations include the static multidimensional subscript (index operator) and the call operator.

Advertisement

Rainer Grimm has been working as a software architect, team leader and training manager for many years. He likes to write articles on the programming languages ā€‹ā€‹C++, Python and Haskell, but also likes to speak frequently at specialist conferences. On his blog Modernes C++ he deals intensively with his passion for C++.

In my last article ā€œC++23: The little gems in the core languageā€ I gave a brief explanation of auto(x) and auto{x}: The calls convert x to a prvalue as if taking x as a function argument would be passed by value. auto(x) and auto{x} perform a decay copy. I also explained decay copy in this context. This explanation of auto(x) and auto{x} was short, too short.

GaŔper Ažman write a tweet about it:

GaÅ”per is an active member of the C++ standardization committee: in C++ he was involved in the standardization of ā€œdeducing thisā€ and ā€œusing enumā€. In addition, GaÅ”per is involved in the standardization of ā€œcustomization pointsā€ and ā€œcontractsā€ and is vice-chair of the Networking Study Group.

Advertisement

I asked GaÅ”per if he would like to write a few words about the impact of auto(x) and auto{x} on functional interface design. Iā€™m happy to present his answer. His explanations are embedded in the source code.

#include
#include
#include
#include
#include
#include
#include
#include
#include

// To achieve a design that improves local reasoning, we should
// design algorithm interfaces to not mutate their arguments if
// at all possible.
// Unfortunately, this is often at odds with the efficiency of
// the implementation of the algorithm.

// This comes up in many areas. A few examples include:
// ā€“ matrix algorithms, where implementations often require
// that arguments do not alias
// ā€“ state machines, where testing is far easier if we can
// produce a completely new state
// ā€“ range algorithms such as the below example
// ā€“ immutable maps and sets (see the immer
// library:

See also  C++23: Deducing This creates explicit pointers

// For clarity, let us consider a small example.
// Our data structure will be a vector of integers
// Our family of algorithms will be ā€œsortedā€ and ā€œuniquedā€

// In c++20 (if we ignore ranges ā€“ we are trying to
// illustrate an approach), we would probably design the
// interface by taking by value and returning by value

// (helpers)
auto read_input(int argc, char** argv) -> std::vector;
auto write_output(int) -> void;

namespace traditional {
template
auto sorted(std::vector x) -> std::vector {
std::sort(x.begin(), x.end());
return x;
}

template
auto uniqued(std::vector x) -> std::vector {
x.erase(std::unique(x.begin(), x.end()), x.end());
return x;
}

// This pattern leads to the following usage pattern
auto usage(int argc, char** argv) {
for (auto i : uniqued(sorted(read_input(argc, argv)))) {
write_output(i);
};
}

// This is good, but sooner or later someone will want to refactor
// this code like this
auto refactor(int argc, char** argv) {
auto input = read_input(argc, argv);
for (auto i : uniqued(sorted(input))) {
write_output(i);
};
}

// can you spot the bug? Non-professionals often donā€™t!
// If you work with researchers and scientists, this kind of
// mistake is ubiquitous, and leads to serious, serious
// slow-downs that are often difficult to find if not spotted
// immediately.

// What can we do? We should ask the compiler to issue an error,
// of course. We can do this by explicitly asking for an rvalue
// reference instead of taking by value.
}

namespace require_moves {
template
auto sorted(std::vector&& x) -> std::vector {
// ^^ new
std::sort(x.begin(), x.end());
return x;
}

template
auto uniqued(std::vector&& x) -> std::vector {
// ^^ new
x.erase(std::unique(x.begin(), x.end()), x.end());
return x;
}
auto read_input(int argc, char** argv) -> std::vector;
auto write_output(int) -> void;

auto usage(int argc, char** argv) {
// compiles unchanged, and has the same performance
for (auto i : uniqued(sorted(read_input(argc, argv)))) {
write_output(i);
};
}

auto refactor(int argc, char** argv) {
auto input = read_input(argc, argv);
for (auto i : uniqued(sorted(std::move(input)))) {
// ^^^^^^^^^^ ^ required, does
// not compile without it!
write_output(i);
};
}

auto print_diff(std::vector const&, std::vector const&) -> void;

// of course, now we have a problem. What if we actually needed
// the copy?
#if defined(TRY_1)
auto check_is_sorted_and_uniqued(int argc, char** argv) {
auto input = read_input(argc, argv);
auto sorted_and_uniqued = uniqued(sorted(input));
// ^^^^^^ no matching function
// for call to sorted
if (input != sorted_and_uniqued) {
print_diff(input, sorted_and_uniqued);
exit(1);
}
exit(0);
}
#elif defined(TRY_2)
// we can work around this by making a copy explicitly
auto check_is_sorted_and_uniqued(int argc, char** argv) {
auto input = read_input(argc, argv);
auto input_copy = input; // (x); }

// If we have that, we could write our check_is_sorted_and_uniqued
// without the named copy:
auto check_is_sorted_and_uniqued(int argc, char** argv) {
auto input = read_input(argc, argv);
auto sorted_and_uniqued = uniqued(sorted(decay_copy(input)));
// ^^^^^^^^^^ ā€œexplicitā€
// copy
if (input != sorted_and_uniqued) {
print_diff(input, sorted_and_uniqued);
exit(1);
}
exit(0);
}

See also  Mozilla Thunderbird: New vulnerability! Multiple vulnerabilities reported

// This works, and in this case is optimal, but leaves something to
// be desired in generic cases. Let us try and see what happens if
// we try and refactor sort+unique into a generic algorithm

#endif
// Letā€™s take our vector as a forwarding reference so we can reuse
// its memory if we own it. We need a concept for that
template
inline constexpr bool is_vector_v = false;
template
inline constexpr bool is_vector_v<:vector a>> = true;
template
concept a_vector = is_vector_v<:remove_cvref_t>>;

// we take this by forwarding reference; but now,
// we make an additional move-construction if v is passed by
// rvalue reference
auto sorted_and_uniqued(a_vector auto&& v) {
return uniqued(sorted(decay_copy(std::forward(v))));
// ^^^^^^^^^^ an extra move construction or
// the needed copy-construction
// specifically, we move-construct decay_copyā€™s return value.
}

// so, decay_copy is clearly not optimal. We need something that
// wonā€™t result in additional move-constructions and still
// accomplish our ā€œcopies are explicitā€ goal.

// enter: decay-copy in the language!
}

namespace done_properly {
using require_moves::sorted, require_moves::uniqued, require_moves::a_vector, require_moves::print_diff;

// in regular user code, we can now use auto{} instead of
// decay-copy:
auto check_is_sorted_and_uniqued(int argc, char** argv) {
auto input = read_input(argc, argv);
auto sorted_and_uniqued = uniqued(sorted(auto(input)));
// ^^^^^^^^^^^ explicit
// copy
if (input != sorted_and_uniqued) {
print_diff(input, sorted_and_uniqued);
exit(1);
}
exit(0);
}

// in generic contexts
auto sorted_and_uniqued(a_vector auto&& v) {
return uniqued(sorted(auto(std::forward(v))));
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// correct forwarded copy of an argument we took by forwarding
// reference
}
}

// Thanks for reading!

More information on how to pass function parameters can be found here: C++ Core Guidelines: Semantics of Function Arguments and Return Values.

See also  Exciting New Features Expected in the 2024 iPad Pro: Design Innovations, M3 Chip, OLED Screen, and More

Thanks to std::mdspan, C++23 supports multidimensional arrays. In addition, the C++23 core language of C++23 offers a multidimensional index operator to complete the feature.

// multidimensionalSubscript.cpp

#include
#include

template
struct Matrix {
std::array mat{};

T& operator[](std::size_t x, std::size_t y) { // (1)
return mat[y * X + x];
}
};

int main() {

std::cout mat;
for (auto i : {0, 1, 2}) {
for (auto j : {0, 1, 2}) mat[i, j] = (i * 3) + j; // (2)
}
for (auto i : {0, 1, 2}) {
for (auto j : {0, 1, 2}) std::cout (1) defines the two-dimensional subscript operator for the class Matrix. (2) use it to define the elements and (3) use it to read out the values.

Here is the output of the program:

With C++23, the call operator (operator ()) and the multidimensional index operator (operator []) be static. The answer to the why question is typically C++: optimization.

optimization

The implicit this pointer must be passed in an extra register when a member function is not called inline. Thanks to a static member function, you can save on this pointer. Lambdas that have no state can also be static in C++23:

auto sum = [](auto a, auto b) static {return a + b;};

For consistency, the multidimensional index operator can also be static.

// multidimensionalSubscriptStatic.cpp

#include
#include

template
struct Matrix {

static inline std::array mat{}; // (2)

static T& operator[](std::size_t x, std::size_t y) { // (1)
return mat[y * X + x];
}
};

int main() {

std::cout mat;
for (auto i : {0, 1, 2}) {
for (auto j : {0, 1, 2}) mat[i, j] = (i * 3) + j;
}
for (auto i : {0, 1, 2}) {
for (auto j : {0, 1, 2}) std::cout Now the two-dimensional index operator (1) and the std::array mat (2) are static.

My next article will be a guest post by Victor Duvanenko. In his article he presents detailed performance figures for my favorite feature in C++17: the parallel STL algorithms. (rm)

To home page

You may also like

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More

Privacy & Cookies Policy