habrahabr

Методы расширения в С++

  • вторник, 21 октября 2014 г. в 03:11:31
http://habrahabr.ru/company/infopulse/blog/240851/

Несколько дней назад Бьёрн Страуструп опубликовал предложение N4174 комитету по стандартизации С++ названное "Call syntax: x.f(y) vs. f(x,y)". Вот вкратце его суть: объявить выражение x.f(y) (вызов для объекта х метода f с аргументом y) эквивалентным выражению f(x,y) (вызов функции f с аргументами x и y). Т.е.

x.f(y) означает:
  1. Попробовать вызвать x.f(y): если класс объекта х содержит метод f, который может принять аргумент y — используем этот метод.
  2. Если пункт №1 не удался — проверяем, существует ли функция f, которая может принять аргументы x и y. Если это так — используем её.
  3. Если не найдено ни того, ни другого — генерируем ошибку.

f(x,y) означает ровно то же самое:
  1. Попробовать вызвать x.f(y): если класс объекта х содержит метод f, который может принять аргумент y — используем этот метод.
  2. Если пункт №1 не удался — проверяем, существует ли функция f, которая может принять аргументы x и y. Если это так — используем её.
  3. Если не найдено ни того, ни другого — генерируем ошибку.

Таким образом мы получаем возможность писать методы расширения, о которых мечтали многие С++ программисты. Я считаю это предложение одним из самых важных в эволюции языка С++.

Методы расширения в 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 пока что похож скорее на исследование теоретических возможностей, чем на формальный стандарт. Есть много разных аспектов, которые должны быть внимательно рассмотрены. Если вам интересно — почитайте документ сами. Тем ни менее, фича выглядит бесспорно полезной и я надеюсь настанет день, когда она войдёт в стандарт языка.