📜 ⬆️ ⬇️

Extension methods in C ++

A few days ago, Bjarne Stroustrup published the N4174 proposal for the C ++ Standardization Committee called " Call syntax: xf (y) vs. f (x, y) ". Briefly, its essence: declare the expression xf (y) (call for object x of method f with argument y ) equivalent to the expression f (x, y) (call of function f with arguments x and y ). Those.

xf (y) means:
  1. Try calling xf (y): if the class of the object x contains a method f that can take the argument y, use this method.
  2. If item 1 failed, check if there is a function f that can accept arguments x and y. If so, use it.
  3. If neither is found, we generate an error.

f (x, y) means exactly the same thing:
  1. Try calling xf (y): if the class of the object x contains a method f that can take the argument y, use this method.
  2. If item 1 failed, check if there is a function f that can accept arguments x and y. If so, use it.
  3. If neither is found, we generate an error.

Thus, we get the opportunity to write extension methods that many C ++ programmers have dreamed of. I consider this sentence one of the most important in the evolution of the C ++ language.

Extension methods in C #


To better understand what we are talking about, let's remember how extension methods are implemented in C #.

The extension method allows you to add functionality to an existing type without modifying the original type or creating an inherited type (and without having to recompile the module containing the original type). Suppose you want to add a method to a string class that counts the number of words in it. To do this, you can write a WordCount method that looks like this (for simplicity, we consider only a space character to be a word separator):
')
static class StringUtilities { public static int WordCount(this string text) { return text.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Length; } } 


Now you can use it like this:

 var text = "This is an example"; var count = text.WordCount(); 

The equivalence of WordCount (text) and text. WordCount () is exactly what Straustrup says in document N4174.

Please note that extension methods in C # have several limitations:



Extension methods in C ++


The question that someone can ask is: “What advantages can xf (y) and f (x, y) have for the language?”. The simple answer is: it makes it possible to define extension methods and use them without changing existing code.

Let's see a real example. Standard containers in C ++ provide a find () method that allows you to find a specific element. But the find () method returns an iterator and you need to check it for end () equality to understand whether the element was found or not. At the same time, often we need not to find the element itself, but to check whether it is contained in a container or not. In standard containers there is no contains () method, but we can write the following function:

 template<typename TKey, typename TValue> bool contains(std::map<TKey, TValue> const & c, TKey const key) { return c.find(key) != c.end(); } 


And call it like this:

 auto m = std::map<int, char> {{1, 'a'}, {2, 'b'}, {3,'c'}}; if(contains(m, 1)) { std::cout << "key exists" << std::endl; } 


But in general, in the world of object-oriented programming, it would be good to write:

 if(m.contains(1)) { } 


In the case when xf (y) and f (x, y) equivalents - the above code is absolutely valid (and beautiful).

Here is a second example. Suppose you want to define some operators similar to those available in LINQ under .NET. Here is an example (simplified) implementation of some such operators for std :: vector.

 template<typename T, typename UnaryPredicate> std::vector<T> where(std::vector<T> const & c, UnaryPredicate predicate) { std::vector<T> v; std::copy_if(std::begin(c), std::end(c), std::back_inserter(v), predicate); return v; } template <typename T, typename F, typename R = typename std::result_of<F(T)>::type> std::vector<R> select(std::vector<T> const & c, F s) { std::vector<R> v; std::transform(std::begin(c), std::end(c), std::back_inserter(v), s); return v; } template<typename T> T sum(std::vector<T> const & c) { return std::accumulate(std::begin(c), std::end(c), 0); } 


Now we can solve a problem like “sum the squares of even numbers from a certain range” like this:

 auto v = std::vector<int> {1,2,3,4,5,6,7,8,9}; auto t1 = where(v, [](int e){return e % 2 == 0; }); auto t2 = select(t1, [](int e){return e*e; }); auto s = sum(t2); 


I don’t like the above code, because it creates a lot of intermediate variables that are needed only to pass to the next call. We can try to get rid of them:
 auto s = sum(select(where(v, [](int e){return e % 2 == 0; }), [](int e){return e*e; })); 


But I like this code even less. Firstly, it is hard to read (too many operations on one line and even other formatting does not help much). Secondly, we see operations in an inverted order as to how they are executed: first we see the call to sum, then select, and only then where. It’s not very convenient to understand where the arguments of one function end and the arguments of the second begin.

However, if the standard of the language determines the equivalence of xf (y) and f (x, y), it will be very easy to write this code:
 auto v = std::vector<int> {1,2,3,4,5,6,7,8,9}; auto s = v.where([](int e){return e % 2 == 0; }) .select([](int e){return e*e; }) .sum(); 


True beautiful? I think so.

Conclusion


Document N4174 is so far more like a study of theoretical possibilities than a formal standard. There are many different aspects that need to be carefully considered. If you're interested, read the document yourself. However, the feature looks undeniably useful and I hope the day will come when it will become a standard language.

Source: https://habr.com/ru/post/240851/


All Articles