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

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

print(x); // 1
print(y); // 2
Деякі зі статично-типізованих мов програмування підтримують механізм перевантаження функцій1. Це виражається в тому, що під одним іменем можна оголосити декілька функцій, які будуть відрізнятись за кількістю аргументів або типами цих аргументів. Характерною ознакою конкретно такої реалізації є те, що перевантаження ігнорує результат. Наслідком такого підходу є неможливість існування двох функцій з однаковими іменами, які відрізняються лише типом результату. Метою цієї сторінки є висловлення ідеї, що подібне обмеження – нісенітниця.
Немає перевантаження – немає проблеми
Існують мови програмування, які не підтримують перевантаження. Очевидних (особисто для мене) причин такої стратегії є декілька:
Мова сформувалась в часи, коли земля ще була пласкою перевантаження ще не винайшли.
Мова не є статично-типізованою.
Автори мови вважають, що перевантаження ускладнює розуміння кінцевої програми.
Серед таких мов, наприклад, мова програмування С та 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 в парі з, несподівано, перевантаженими функціями. Якщо спробувати ініціалізувати змінну оголошену через 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()
Висновки
Перевантаження функцій за результатом не є життєво-важливим механізмом для мови. Тим не менше, подібне твердження можна сказати як про перевантаження загалом, так і про інші можливості та особливості мов.
Більше того, важко оцінити необхідність і вигідність механізму до його повноцінного впровадження. Лише після випробувань на практиці ми можем сміливо стверджувати про актуальність того чи іншого підходу. Сама ж відсутність можливості провести такі випробування лише обмежує вже наші власні можливості в роботі, в тому числі й до пошуку нових, ще не відкритих засобів.
Але найбільш суперечливим моментом в цій історії є те, що ті проблеми, які заважають реалізувати механізм перевантаження за результатом, успішно подолані в інших областях. Подібне відношення не лише робить будь-яку мову менш повною, але й є проявом несправедливості і лицемірства зі сторони розробників мови. Або ж просто говорить про їх несерйозне відношення до роботи.
Підсумовуючи все вищезгадане я можу прийти до висновку, що подібний механізм явно не буде зайвим, а його відсутність лише збіднює мову.
1
Я буду використовувати терміни перевантаження або перевантаження функцій маючи на увазі загальний підхід, який також включає перевантаження методів, операторів, функторів, etc.