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

Несколько дней назад Бьёрн Страуструп опубликовал предложение 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 bool contains (std: map const & c, TKey const key) { return c.find (key) != c.end (); } И вызывать её вот так:

auto m = std: map {{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 std: vector where (std: vector const & c, UnaryPredicate predicate) { std: vector v; std: copy_if (std: begin©, std: end©, std: back_inserter (v), predicate); return v; } template :: type> std: vector select (std: vector const & c, F s) { std: vector v; std: transform (std: begin©, std: end©, std: back_inserter (v), s); return v; } template T sum (std: vector const & c) { return std: accumulate (std: begin©, std: end©, 0); } Теперь задачу типа «просуммировать квадраты чётных чисел из некоторого диапазона» мы можем решить вот так:

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

© Habrahabr.ru