Мысли вслух. Перегрузка за результатом.
Пример кода №1
Использование перегруженных за результатом функций в представлении художника
int f() {
return 1;
}
float f() {
return 2.0;
}

int x = f();
float y = f();

print(x); // 1
print(y); // 2
Некоторые статически-типизированные языки программирования поддерживают механизм перегрузки функций. Это выражается в том, что под одним именем можно объявить несколько функций, которые отличаются количеством аргументов или типами этих аргументов. Характерной чертой конкретно такой реализации является то, что такая перегрузка игнорирует результат. В следствии этого невозможно создать две функции с одинаковыми именами, которые отличаются только типом результата. В этой статье я хотел бы выразить идею, что подобное ограничение - чепуха.
Нет перегрузки - нет проблем
Существуют языки программирования, которые не поддерживают перегрузку. Очевидных (лично для меня) причин такой стратегии несколько:
Язык сформировался во времена, когда земля еще была плоской перегрузку еще не изобрели.
Язык не является статически-типизированным.
Авторы языка считают, что перегрузка усложняет понимание конечной программы.
Среди таких языков, например, язык программирования С и JavaScript. Принимая в расчет хотя бы солидный возраст языка С (46 лет на момент написания этой статьи) – можно сделать следующий вывод: жизнь без перегрузки все же есть. Таким образом я надеюсь что мы согласны по крайней мере в том, что это в некоторой мере вопрос вкуса.
Исключения
Своего рода исключение создает тот нюанс, что перегрузка базируется на сигнатуре функции. А она, по крайней мере в некоторых языках, может включать в себя дополнительные компоненты. Например, в языке программирования С++ возможна перегрузка метода за типом результата, но только если два таких метода отличаются квалификатором const (см. Пример кода №2). Таким образом это исключение не исключение вовсе, так как разница в методах все же включает в себя не один лишь результат. Но не выделить этот случай в отдельную категорию я все же не могу, так как при поверхностном анализе создается видимость именно того, что нам и надо - перегрузки за результатом.
Пример кода №2
Использование квалификатора const может создать иллюзию перегрузки за результатом
class X {
int f();
float f() const;
};
Аргументация
Один из аргументов против перегрузки за результатом, который в то же время не относиться к категории «программа перестанет быть читабельной» - это принципиальная неопределенность выбора между двумя такими функциями (по крайней мере при отсутствии каких-либо ориентиров). И этот аргумент абсолютно небезосновательный. Действительно, вызов такой «двухзначной» функции не дает никакой возможности угадать, что именно имел ввиду разработчик (см. Пример кода №3). Функция, возвращающая int ничем не лучше функции, возвращающей float, так какую же из них разработчик хотел вызвать на самом деле? Определить это, кроме как случайным образом, не представляется возможным.
Пример кода №3
Исходя из кода не очевидно какую именно функцию необходимо вызвать
int f();
float f();

f(); // неоднозначность
Контраргументация
Но это все кажется каким-то знакомым, где-то мы уже это видели... по крайней мере в языке программирования С++ уже существуют примеры схожих неоднозначностей.
Например, два пространства имен содержащие объекты с одинаковыми именами. Если вложенные объекты таких пространств развернуть в текущую область при помощи using namespace (см. Пример кода №4), то при обращении к таким объектам-близнецам компилятор не сориентируется который из них на самом деле нам необходим и остановит компиляцию. Разработчику придется разрешать эту неоднозначность при помощи оператора ::, явно указав внутри которой из областей находиться требуемый объект.
Пример кода №4
Не очевидно к какой именно функции идет обращение
namespace A {
void f();
}
namespace B {
void f();
}

using namespace A;
using namespace B;

f(); // неоднозначность
A::f(); // решение
Еще одним примером является использование оператора auto в паре с, неожиданно, перегруженными функциями. Если попытаться инициализировать переменную объявленную через это ключевое слово перегруженной функцией, то компилятор не сможет вывести ее тип (см. Пример кода №5). Разработчику придется либо убрать auto, либо использовать static_cast и привести функцию к желаемой сигнатуре, таким образом явно указав чем он хочет инициализировать переменную.
Пример кода №5
Явное приведение перегруженной функции к определенной сигнатуре
void f();
void f();

auto g = f; // неоднозначность
auto h = static_cast<void(*)(float)>(f); // решение
Вопросы
А что, собственно говоря, мешало задействовать такой же механизм при работе с перегруженными за результатом функциями? Можно было бы добавить специальный оператор для явного указания возвращаемого типа. Либо повторно использовать static_cast для решения этой неоднозначности. Исчерпывающих и в то же время приемлемых аргументов я не нахожу. Само собой кроме тех, которые относятся к категории «программа станет нечитабельной».
Возможности
Тем не менее, данный подход имеет несколько естественных свойств, которые минимизируют возможность возникновения неоднозначностей.
В первую очередь можно рассмотреть вариант, когда среди перегруженных функций встречается и вариация, которая не возвращает результат вовсе. Было бы логично отдавать предпочтение в ее пользу, когда результат функции никуда не передается и нигде не сохраняется (см. Пример кода №6).
Пример кода №6
Выбор функции без результата по умолчанию
void f();
int f();

f(); // void f()
Автоматизацию процесса разрешения неоднозначностей можно было бы реализовать по аналогии с выведением в шаблонах (см. Пример кода №7). Например, если результат «неоднозначной функции» передается в другую функцию, то выбрать нужно именно тот вариант, результат которого совпадает с аргументом.
Пример кода №7
Вызов функции, результат которой совпадает с типом аргумента
int f();
float f();

void g(float);

g(f()); // float f()
Выводы
Перегрузка функций за результатом определенно не является жизненно важным механизмом для языка. Тем не менее, подобно высказывание справедливо и для перегрузки в общем, и для других возможностей и особенностей.
Более того, тяжело оценить необходимость и выгоду от механизма до его полноценного внедрения. Только после испытаний на практике мы можем смело заявлять о актуальности того или иного подхода. Отсутствие же возможности такие испытания провести лишь ограничивает наши возможности в разработке, в том числе и к поиску новых, еще не открытых средств.
Но наиболее противоречивым моментом в этой истории есть то, что проблемы мешающие реализации перегрузки за результатом успешно преодолены в других областях. Подобное отношение не только делает любой язык менее полным, но и является проявлением несправедливости и лицемерия со стороны разработчиков этого языка. Либо же просто говорит о несерьезном отношении к работе.
Подбивая итоги, я могу прийти к выводу, что подобный механизм явно не будет лишним, а его отсутствие лишь обедняет язык.