Once again about methods of extending classes in C++
Many popular modern object-oriented languages (C#, Kotlin, Swift, Dart) have such a mechanism as extension methods. It allows you to add the necessary methods and properties to a class from the outside without changing the class itself. This is a very convenient and useful mechanism when, for example, we want to add helper methods to some library class that we do not own and cannot modify directly. For example, add your own method to any third-party class toString()
.
On the example of Kotlin, it looks very simple and it is clear what happens in principle to anyone who is not even familiar with Kotlin:
class Person(val name: String)
fun Person.greet() = "Hello $name!"
fun main() {
val person = Person("John")
println(person.greet())
}
Another advantage of extensions is the ability to implement call chains in a functional style, adding the necessary methods not only to your classes, but also by extending third-party ones. Real life examples are LINQ in C#, Sequences in Kotlin, Streams in Java. Another example:
listOf(1, 2, 3).map { it.toString() }.joinToString(“,”)
If it is written in the form of a normal function call, then it will not be so good, agree:
joinToString(map(listOf(1,2,3) {it.toString}), “,”)
We observe a large amount of nesting, a variety of opening and closing parentheses, the code is more difficult to read, it is possible to get confused which parameters are passed to which function.
Many dynamic languages also think they have extension methods, but this is not entirely true. Generally, in JavaScript, Ruby, and other dynamic languages, we can dynamically add a method to the class itself. Outwardly, it is similar to extension methods. But this method is quite dangerous. First, all other users of this class will see this method, and secondly, you can break the class if you accidentally override its existing method. For example, Vasya adds a method to some system class in his library toString()
you, connecting the Vasya library, do not suspect this and add your method toString()
in the same class, thereby breaking the logic of the Vasi library. Therefore, in dynamic languages, this technique of extending classes is not common and is punished.
Unfortunately, despite the pace of C++ development, which has accelerated in recent years, and the addition of a bunch of useful features, such as coroutines, concepts, extensions to the standard library, ranges, threads.. the extension mechanism is somehow ignored, although in my opinion it is a fairly simple feature, which doesn’t really break the existing syntax. Even Straustrup proposed to add them in 2014, 10 years have passed, but, unfortunately, we still do not see it in the language and it is not known when we will see it.
In part, this approach is implemented in the std::ranges library, there is a so-called pipe syntax:
auto const ints = {0, 1, 2, 3, 4, 5};
auto even = [](int i) { return 0 == i % 2; };
auto square = [](int i) { return i * i; };
// the "pipe" syntax of composing the views:
for (int i : ints | std::views::filter(even) | std::views::transform(square))
std::cout
It is extensible, you can write your own transformation. I didn’t know about it, but I think it’s not difficult there 🙂
Agree, very similar to the methods of extensions.
For me, there are two small “inconvenient” points of using ranges as extension methods, firstly, it is only starting with C++20, which is far from being introduced everywhere, or has been introduced, but compatibility with old compilers needs to be maintained. And the second point, ranges work on collections, not on single objects. By the way, I have little experience with them, maybe I will be corrected and I invented the bicycle.
In general, I hope I managed to open up the topic, you can move on to practice.
I want to share my rather simple way of implementing extension methods, while repeating the pipe syntax from the ranges library.
The simplest example of adding a method toInt()
to the line:
#include
struct ToIntParams {};
inline ToIntParams toInt() { return {}; }
inline int operator|(const std::string& s, const ToIntParams&) {
return std::stoi(s);
}
int main() {
std::cout
In this example, we create an empty structure ToIntParams
and overload the operator “|
” for a line and this structure, we write all the functionality we need in an overloaded operator. Function toInt()
– Helpful to shorten the code. A parameter structure object can be created without this function.
In the following example, I will show how to add real parameters to the extension method:
#include
#include
struct JoinStringParams {
const char* delimiter;
};
inline JoinStringParams joinToString(const char* delimiter) { return { delimiter }; }
template
std::string operator|(const Iterable& iterable, const JoinStringParams& m) {
std::stringstream ss;
bool first = true;
for (const auto& v : iterable) {
if (first) first = false; else ss
We can write our transformation method, which accepts a transformation function or lambda as input:
#include
#include
template
struct TransformParams {
const Func& func;
};
template
inline TransformParams transform(const Func& func) { return { func }; }
template ::type>
inline Out operator|(const In& in, const TransformParams& p) {
return p.func(in);
}
std::string oddOrEven(int i) {
return i % 2 == 0 ? "even" : "odd";
}
int main() {
std::cout
In general, this is the whole idea, now you can write various helper functions in a style that is very similar to extension methods.
There are truths and flaws:
-
you need to write more code to implement such a function,
-
different IDEs do not always understand which implementation to switch to when clicking on the ” statement
|
” -
uninitiated programmers may not understand what is happening, although the usage looks quite obvious
-
operator “
|
” has a rather low priority and sometimes you have to enclose the entire expression in parentheses, for example, in the case of using an operatorкак в моих примерах
Finally, one more example of data model serialization. I gave an example here with some Node, but you can also use something from real life, for example QJsonDocument
if there is Qt and it is necessary to serialize in JSON.
#include
#include
#include
#include
Thank you for your attention.