Добавить расширяющие класс/структуру методы.

rumyancev95
rumyancev95

Очень часто хочется расширить существующий объект каким-либо методом, при этом не затронув интерфейс и структуру поставляемего класса.

 

Если не трогать код поставляемого класса, сейчас принято делать свободные функции или перегружать операторы, которые будут принимать экземпляр существующего класса в том или ином виде и производить определенные над ним действия.

 

Предлагаю ввести новую конструкцию в язык, которая будет делать весь этот процесс под капотом, при этом, на уровне синтаксиса предлагать пользователю интерфейс вызова нового метода, как будто он действительно принадлежит экземпляру класса.

 

Далее будет приведен пример того как это могло бы выглядеть. На лучшее наименование ключевого слова не претендую. В идеале обходиться без новых ключевых слов, но в голову других вариантов не приходит.

 

Приведу пример по все той же "наболевшей"  реализацией метода .split.

Вот так будет выглядеть определение расширяющего класс/структуру метода:

std::vector<std::string> std::string::split(const std::string& delim) extension
{
    /// Здесь какая-либо реализация логики данной функции...

    /// this - получение константного указателя на экземпляр класса, для которого создаем расширяющий метод
    auto pos = this->find(delim);
    
    /// ...

    return  std::vector<std::string>(res);
}

Компилятором этот метод будет преобразован в нечто:

 

std::vector<std::string> compiler_some_name_split(std::string* const str, const std::string& delim);

Который будет откликаться на синтаксис .split:

std::string str;
auto res = str.split("\n");

Все равно, что бы мы вызвали:

std::string str;
auto res = compiler_some_name_split(&str, "\n");

 

Чисто концептуальный второй пример для наглядности, абстрагируясь от строк:

 

struct A
{
    int value;
    int foo() { return value; }
};


int A::bar() extension
{
    return this->foo();
}

// Компилятором будет создано со своим каким он захочет именем:
int _bar(A* const a)
{
    return a->foo();
}

int main()
{
    A a{42};
    std::cout << a.bar(); // 42
    // Вместо конструкции выше по факту будет вызвано
    std::cout << _bar(&a);
}

 

И последний пример, если мы захотим добавить расширяющий метод для шаблонного класса:

template <class T>
struct A
{
    T value;

    T foo() { return value; }
};

/// Таким образом мы создаем расширяющий метод для всех типов шаблонного класса A
template<class T>
T A<T>::bar() extension
{
    return this->foo();
}

/// Такую функцию сгенерирует компилятор с именем, которое он захочет
template <class T>
T _bar(A<T>* const a)
{
    return a->foo();
}


int main()
{
    A<int> a{42};
    std::cout << a.bar(); // 42
    /// Вместо конструкции выше будет выполнена эта:
    std::cout << _bar(&a);
}

 

По сути - это добавление синтаксического сахара для вызова методов.

Приму любую критику, почему данная фича не может быть добавлена в ядро языка. Реализовать такое компилятору думаю будет совсем не сложно.

 

 

-1
рейтинг
21 комментарий
ru.night.beast
разговоры о uniform function call syntax давно ходят.
см. например
open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4174.pdf
open-std.org/Jtc1/sc22/wg21/docs/papers/2015/p0079r0.pdf
правда, не известно, на какой стадии это дало находится.
ru.night.beast
rumyancev95
ru.night.beast, Делать это не явно как в Dlang не хочется. Я так понял в предложениях именно о неявном преобразовании из foo(&a, arg) в a.foo(arg) идет речь. Мне кажется, такое смелое неявное преобразование без явного указания инструкции - не путь с++.
rumyancev95
Antervis
rumyancev95, полагаю, Страуструпу виднее, что "путь с++", а что нет. Важно на самом деле то, что UFC позволит реализовывать некоторые шаблонные/синтаксические трюки намного проще. Простой пример: существует sort(RandomAccessContainer &c). Некоторые контейнеры предоставляют свой метод sort, т.к. стандартный не подходит. Тогда в одном случае это c.sort(), а в другом sort(c), либо через обертку с sfinae-перегрузкой. С UFC можно просто писать c.sort().
Antervis
rumyancev95
Antervis, Не знаю. Мне кажется объединять в одну сущность a.foo(b) и foo(a, b) по умолчанию, если аргумент `a` есть ссылка на какой либо объект - слишком громкое новводение и оно по идее тогда будет распространяться на любые объекты или только пользовательские? И если мы определим такую внешнюю функцию, а в foo уже будет функция с той же сигнатурой, нам что, компилятор запретит cделать такую функции? или не даст вызвать ее через a.foo(b)? Введение UFC в этом формате может поломать существующий код.
rumyancev95
Antervis
rumyancev95, a.foo(b) вызовет свободную функцию foo(A&, B) только если A::foo(B) не существует и наоборот. Поэтому UFC не поломает существующий код, а лишь определит поведение ныне некорректного.
Antervis
Дмитрий Назаров
А не приведет ли это к тому, что отлаживать код станет еще сложнее? Сейчас, увидев код вида foo.bar(), можно утверждать, что bar является либо методом одного из классов в иерархии наследования, либо функицией/функтором. Если будет возможность добавлять новые методы на ходу, то уже сложнее будет определить, что это такое. Конечно, современные среды разработки все могут, но как минимум при беглом просмотре это будет сбивать с толку.

Кроме того, а как быть, если имя расширения конфликтует с именем метода в классе потомке? Явный вызов метода класса через его full qualified name поможет вызвать метод потомка. А как вызвать именно расширение?
Дмитрий Назаров
rumyancev95
Дмитрий Назаров, при отладке мы будем падать в расширяющий метод, тут не вижу проблем.

Согласен с тем, что этой новой возможностью не удастся явно узнать что такое a.foo(). Раньше мы предполагали что это может быть метод или функтор. Теперь появится третий вариант. Тут ничего не поделаешь.

Если у класса есть метод с сигнатурой которую мы хотим расширить - то ошибка компиляции. Расширяющие методы не должны влиять на внутренний мир класса.
rumyancev95
rumyancev95
Дмитрий Назаров, И да, расширяющий метод не добавляет по факту никакого метода классу. При наследовании объекта, который имеет расширяющий метод, не будет получено расширение. Если держать в голове, что расширение это внешний от класса метод, то все встает на свои места.
rumyancev95
Дмитрий Назаров
rumyancev95, так в том и вопрос. Если, скажем, я в наследнике определю foo, то a.foo() должно вызывать метод наследника, как я понимаю? Ведь при наследовании мы ничего не можем знать о расширении, так как оно может быть объявлено постфактум где-то еще.
Дмитрий Назаров
rumyancev95
Дмитрий Назаров, расширения не наследуются и соответственно не могут быть вызваны через наследника. Расширение привязано строго к типу класса. Все потому, что расширения классов/структур не находсят внутри. По факту, они компилятором будут построены снаружи.
rumyancev95
Дмитрий Назаров
rumyancev95, по поводу наследования расширений тут надо подумать. Если расширения не могут использоваться потомком, то не противоречит ли это наследованию?
Дмитрий Назаров
Дмитрий Назаров
Вопрос не в том, может ли потомок вызывать расширение, а в том, можно ли расширение применять к потомку. И тут интересно как вызвать именно расширение, если в потомке есть метод с таким же именем.
Дмитрий Назаров
rumyancev95
Дмитрий Назаров, Что вы понимаете под фразой "применить расширение к потомку"? Мы всегда создаем расширение для конкретного типа и в результате, к указателю на этот тип будет создан внешний метод. Это все независимая от наследования конструкция. Если я ошибаюсь, приведите пожалуйста пример.
rumyancev95
Дмитрий Назаров
rumyancev95, Пример:

struct B : A
{
void bar();
);

B b;
b.bar(); // тут очевидно, что вызывается B::bar

А как вызвать именно ваше расширение применительно к объекту b?
Дмитрий Назаров
Дмитрий Назаров
Дмитрий Назаров, если имя выбирает компилятор, то это как минимум не переносимо.
Дмитрий Назаров
Дмитрий Назаров
Дмитрий Назаров, логично было бы вызывать расширение через full qualified name для класса, для которого определялось расширение. Но тогда это выглядеть будет так себе. А если учесть, что основная цель - синтаксический сахар, то это несколько снижает ценность предложения.
Еще вопрос как это должно вести себя с ODR. Если, например, я в разных заголовках объявлены разные расширения с одинаковым именем.
Дмитрий Назаров
Комментарий удален
Дмитрий Назаров
А как расширение должно вести себя с классами, для которых есть только forward declaration?
Дмитрий Назаров
rumyancev95
Дмитрий Назаров,
> А как вызвать именно ваше расширение применительно к объекту b?
Никак, так как для класса B не определен расширяющий метод, даже несмотрся на то, что он наследуется от A, у которого есть расширение. Потому что расширение не расширяетв нутренний мир класса, а лишь создает внешнюю функцию на указанный тип.

-> если имя выбирает компилятор, то это как минимум не переносимо.
Компилятор определяет у себя внутри мета-имена которые пользователь никогда не вызовет. Это необходимая ему справочная информация чтобы реагировать на вызов расширяющих функций у объектов через точку. Это кишки компилятора, переносимость кода здесь не причем.

> А как расширение должно вести себя с классами, для которых есть только forward declaration?

Также как и с обычными функциями. Вы не сможете определить функцию для незаконченного типа. Будет ошибка компиляции. Тоже самое с расширяющими функциями.
rumyancev95
Дмитрий Назаров
rumyancev95, при наличии forward declaration можно определять функции, для которых эти объекты передаются по указателю или по ссылке.
Так а почему нельзя применить расширение для наследника? Если само расширение это всего лишь синтаксис для вызова, например, bar(&a). То поясните мне, чем это противоречит вызову bar(&b).
Дмитрий Назаров
Дмитрий Назаров
Вот еще интересный вопрос. Операторы присваивания и операторы преобразования типа точно такие же методы класса, как и любые другие. Можно ли добавлять их? Если да, то не сломает ли это инкапсуляцию?
Дмитрий Назаров
rumyancev95
Дмитрий Назаров, Операторы которые вы назвали по стандарту не могут быть не членами класса. Соответственно расширять их не представится возможным.
rumyancev95
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).