Несколько дней назад Бьёрн Страуструп опубликовал предложение N4174 комитету по стандартизации С++ названное "Call syntax: x.f(y) vs. f(x,y)". Вот вкратце его суть: объявить выражение x.f(y) (вызов для объекта х метода f с аргументом y) эквивалентным выражению f(x,y) (вызов функции f с аргументами x и y). Т.е.
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(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 пока что похож скорее на исследование теоретических возможностей, чем на формальный стандарт. Есть много разных аспектов, которые должны быть внимательно рассмотрены. Если вам интересно — почитайте документ сами. Тем ни менее, фича выглядит бесспорно полезной и я надеюсь настанет день, когда она войдёт в стандарт языка.
Автор: tangro