Несколько дней назад Бьёрн Страуструп опубликовал предложение N4174 комитету по стандартизации С++ названное "Call syntax: x.f(y) vs. f(x,y)". Вот вкратце его суть: объявить выражение x.f(y) (вызов для объекта х метода f с аргументом y) эквивалентным выражению f(x,y) (вызов функции f с аргументами x и y). Т.е.
x.f(y) означает:
f(x,y) означает ровно то же самое:
Таким образом мы получаем возможность писать методы расширения, о которых мечтали многие С++ программисты. Я считаю это предложение одним из самых важных в эволюции языка С++.
Чтобы лучше понять о чём мы говорим, давайте вспомним как методы расширения реализованы в С#.
Метод расширения позволяет вам добавить функциональность к существующему типу без модификации оригинального типа или создания унаследованного типа (и без необходимости перекомпиляции модуля, содержащего оригинальный тип). Предположим, вы хотите добавить к классу строки метод, подсчитывающий количество слов в ней. Для этого вы можете написать метод WordCount выглядящий вот так (для простоты будем считать разделителем слов один лишь символ пробела):
Теперь вы можете использовать его вот так:
Эквивалентность WordCount(text) и text.WordCount() это именно то, о чём говорит Страуструп в документе N4174.
Обратите внимание, что методы расширения в С# имеют несколько ограничений:
Вопрос, который кто-то может задать: «Какие преимущества может дать эквивалентность x.f(y) и f(x,y) для языка?». Простой ответ: это даёт возможность определять методы расширения и использовать их без изменения уже существующего кода.
Давайте посмотрим реальный пример. Стандартные контейнеры в С++ предоставляют метод find(), позволяющий найти определённый элемент. Но метод find() возвращает итератор и вам необходимо проверять его на равенство end() для понимания того, был элемент найден или нет. В то же время часто нам нужно не найти сам элемент, а проверить, содержится ли он в контейнере или нет. В стандартных контейнерах нет метода contains(), но мы можем написать вот такую функцию:
И вызывать её вот так:
Но вообще-то в мире объектно-ориентированного программирования хорошо было бы написать:
В случае когда x.f(y) и f(x,y) эквиваленты — вышеуказанный код абсолютно валиден (и красив).
Вот второй пример. Допустим вы хотите определить некоторые операторы, аналогичные имеющимся в LINQ под .NET. Вот примерная (упрощенная) реализация некоторых таких операторов для std::vector.
Теперь задачу типа «просуммировать квадраты чётных чисел из некоторого диапазона» мы можем решить вот так:
Вышеуказанный код мне не нравится, поскольку создаётся много промежуточных переменных, нужных лишь для передачи в следующий вызов. Мы можем попробовать избавиться от них:
Но этот код нравится мне ещё меньше. Во-первых, его тяжело читать (слишком много операций в одной строке и даже другое форматирование не очень помогает). Во-вторых, мы видим операции в инвертированном порядке относительно того, как они выполняются: сначала мы видим вызов sum, затем select и лишь потом where. Понять где заканчиваются аргументы одной функции и начинаются аргументы второй тоже не очень удобно.
Однако если стандарт языка определит эквивалентность x.f(y) и f(x,y), будет очень просто написать вот такой код:
Правда, красивый? Мне кажется — да.
Документ N4174 пока что похож скорее на исследование теоретических возможностей, чем на формальный стандарт. Есть много разных аспектов, которые должны быть внимательно рассмотрены. Если вам интересно — почитайте документ сами. Тем ни менее, фича выглядит бесспорно полезной и я надеюсь настанет день, когда она войдёт в стандарт языка.
x.f(y) означает:
- Попробовать вызвать x.f(y): если класс объекта х содержит метод f, который может принять аргумент y — используем этот метод.
- Если пункт №1 не удался — проверяем, существует ли функция f, которая может принять аргументы x и y. Если это так — используем её.
- Если не найдено ни того, ни другого — генерируем ошибку.
f(x,y) означает ровно то же самое:
- Попробовать вызвать x.f(y): если класс объекта х содержит метод f, который может принять аргумент y — используем этот метод.
- Если пункт №1 не удался — проверяем, существует ли функция f, которая может принять аргументы x и y. Если это так — используем её.
- Если не найдено ни того, ни другого — генерируем ошибку.
Таким образом мы получаем возможность писать методы расширения, о которых мечтали многие С++ программисты. Я считаю это предложение одним из самых важных в эволюции языка С++.
Методы расширения в C#
Чтобы лучше понять о чём мы говорим, давайте вспомним как методы расширения реализованы в С#.
Метод расширения позволяет вам добавить функциональность к существующему типу без модификации оригинального типа или создания унаследованного типа (и без необходимости перекомпиляции модуля, содержащего оригинальный тип). Предположим, вы хотите добавить к классу строки метод, подсчитывающий количество слов в ней. Для этого вы можете написать метод WordCount выглядящий вот так (для простоты будем считать разделителем слов один лишь символ пробела):
static class StringUtilities { public static int WordCount(this string text) { return text.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Length; } }
Теперь вы можете использовать его вот так:
var text = "This is an example"; var count = text.WordCount();
Эквивалентность WordCount(text) и text.WordCount() это именно то, о чём говорит Страуструп в документе N4174.
Обратите внимание, что методы расширения в С# имеют несколько ограничений:
- метод расширения всегда должен быть объявлен как public static метод статического класса
- метод расширения имеет доступ полько к public методам и свойствам расширяемого типа
Методы расширения в С++
Вопрос, который кто-то может задать: «Какие преимущества может дать эквивалентность x.f(y) и f(x,y) для языка?». Простой ответ: это даёт возможность определять методы расширения и использовать их без изменения уже существующего кода.
Давайте посмотрим реальный пример. Стандартные контейнеры в С++ предоставляют метод find(), позволяющий найти определённый элемент. Но метод find() возвращает итератор и вам необходимо проверять его на равенство end() для понимания того, был элемент найден или нет. В то же время часто нам нужно не найти сам элемент, а проверить, содержится ли он в контейнере или нет. В стандартных контейнерах нет метода contains(), но мы можем написать вот такую функцию:
template<typename TKey, typename TValue> bool contains(std::map<TKey, TValue> const & c, TKey const key) { return c.find(key) != c.end(); }
И вызывать её вот так:
auto m = std::map<int, char> {{1, 'a'}, {2, 'b'}, {3,'c'}}; if(contains(m, 1)) { std::cout << "key exists" << std::endl; }
Но вообще-то в мире объектно-ориентированного программирования хорошо было бы написать:
if(m.contains(1)) { }
В случае когда x.f(y) и f(x,y) эквиваленты — вышеуказанный код абсолютно валиден (и красив).
Вот второй пример. Допустим вы хотите определить некоторые операторы, аналогичные имеющимся в LINQ под .NET. Вот примерная (упрощенная) реализация некоторых таких операторов для 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); }
Теперь задачу типа «просуммировать квадраты чётных чисел из некоторого диапазона» мы можем решить вот так:
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);
Вышеуказанный код мне не нравится, поскольку создаётся много промежуточных переменных, нужных лишь для передачи в следующий вызов. Мы можем попробовать избавиться от них:
auto s = sum(select(where(v, [](int e){return e % 2 == 0; }), [](int e){return e*e; }));
Но этот код нравится мне ещё меньше. Во-первых, его тяжело читать (слишком много операций в одной строке и даже другое форматирование не очень помогает). Во-вторых, мы видим операции в инвертированном порядке относительно того, как они выполняются: сначала мы видим вызов sum, затем select и лишь потом where. Понять где заканчиваются аргументы одной функции и начинаются аргументы второй тоже не очень удобно.
Однако если стандарт языка определит эквивалентность x.f(y) и f(x,y), будет очень просто написать вот такой код:
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();
Правда, красивый? Мне кажется — да.
Вывод
Документ N4174 пока что похож скорее на исследование теоретических возможностей, чем на формальный стандарт. Есть много разных аспектов, которые должны быть внимательно рассмотрены. Если вам интересно — почитайте документ сами. Тем ни менее, фича выглядит бесспорно полезной и я надеюсь настанет день, когда она войдёт в стандарт языка.

