Перегрузка функции по типу возврата?

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

HTML, CSS dan JS
HTML, CSS dan JS
HTML (HyperText Markup Language) - это язык разметки, используемый для создания и оформления веб-страниц. HTML описывает структуру и содержание...
Каковы некоторые из продвинутых концепций языков программирования?
Каковы некоторые из продвинутых концепций языков программирования?
В языках программирования существует множество продвинутых концепций, но некоторые из них являются наиболее важными:
Основы программирования на Java
Основы программирования на Java
Java - это высокоуровневый объектно-ориентированный язык программирования, основанный на классах.
260
1
98 785
13

Ответы 13

На таком языке, как бы вы разрешили следующее:

f(g(x))

если у f были перегрузки void f(int) и void f(string), а у g были перегрузки int g(int) и string g(int)? Вам понадобится какой-нибудь устранитель неоднозначности.

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

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

Jay Conrod 14.01.2009 09:52

да, стандартные преобразования ранжируются на точное совпадение, продвижение и преобразование: void f (int); void f (длинный); f ('а'); вызывает f (int), потому что это всего лишь продвижение, а преобразование в long - это преобразование. void f (float); void f (короткий); f (10); потребует преобразования для обоих: вызов неоднозначен.

Johannes Schaub - litb 14.01.2009 10:58

Если язык имеет ленивую оценку, это не такая уж большая проблема.

jdd 30.10.2010 09:08

Upvote, взаимодействие перегрузки типа параметра и перегрузки типа возвращаемого значения не рассматривается в сообщении Рекса. Очень хороший момент.

Joseph Garvin 01.11.2010 02:11

@jeremiahd: Почему ленивая оценка упрощает задачу?

Joseph Garvin 01.11.2010 02:11

Если бы я разрабатывал язык, моим правилом было бы то, что для любой перегруженной функции каждая сигнатура параметра должна иметь один возвращаемый тип, назначенный по умолчанию; компилятор начал бы с предположения, что каждый вызов функции будет использовать тип по умолчанию. Однако как только это будет сделано, в каждой ситуации, когда возвращаемое значение функции было немедленно приведено или принудительно приведено к чему-то еще, компилятор будет проверять наличие перегрузки, сигнатура параметра которой идентична, но чей возвращаемый тип лучше соответствует (или, возможно, недействителен). . Я бы, вероятно, также ввел правило «переопределить один - переопределить все» для таких перегрузок.

supercat 06.01.2012 02:05

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

supercat 06.01.2012 02:11

Почему Java также не допускает разный возврат при наследовании? (Я знаю о переопределении, но почему Java не применяет концепцию сокрытия)? вот полный вопрос.

Asif Mushtaq 12.03.2016 15:06

Чтобы украсть специфичный для C++ ответ на другой очень похожий вопрос (обман?):


Типы возвращаемых функций не участвуют в разрешении перегрузки просто потому, что Страуструп (я предполагаю, что с вводом от других архитекторов C++) хотел, чтобы разрешение перегрузки было «контекстно-независимым». См. Раздел 7.4.1 - «Перегрузка и возвращаемый тип» в «Языке программирования C++, третье издание».

The reason is to keep resolution for an individual operator or function call context-independent.

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

И, черт его знает, разрешение перегрузки C++ достаточно сложно в существующем виде ...

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

int func();
string func();

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

void main() 
{
    func();
}

По этой причине разработчики языка часто запрещают перегрузку возвращаемого значения.

Однако некоторые языки (например, MSIL) делать допускают перегрузку по типу возвращаемого значения. Конечно, они тоже сталкиваются с вышеуказанной трудностью, но у них есть обходные пути, для которых вам придется обращаться к их документации.

Небольшая придирка (ваш ответ дает очень четкое и понятное обоснование): дело не в том, что нет никакого пути; просто пути будут неуклюжими и более болезненными, чем хотелось бы большинству людей. Например, в C++ перегрузку можно было бы разрешить с помощью некрасивого синтаксиса приведения типов.

Michael Burr 14.01.2009 20:25

Но Майкл, как можно разрешить вызов, подобный приведенному выше (func ();), без того, чтобы сам пользователь помог компилятору дополнительными подсказками? Я не думаю, что это вообще возможно.

Frederick The Fool 14.01.2009 20:41

Возвращаемое значение нигде не используется, поэтому не имеет значения, какой из двух будет вызван. Фактически, вся ваша функция main () не работает.

Jörg W Mittag 14.01.2009 20:55

@Frederick - я согласен с тем, что пользователю потребуется предоставить компилятору дополнительную информацию, чтобы это работало (обратите внимание, что я нет говорит, что это действительно работает - просто это могло бы, если бы разработчики языка сочли это ценным). В C++ они, вероятно, сделали бы это, используя какое-то приведение.

Michael Burr 14.01.2009 21:03

@ Jörg W Mittag: Вы не видите, что делают функции. Они легко могли иметь побочные эффекты разные.

A. Rex 14.01.2009 21:12

Если бы у них были побочные эффекты, они бы не были функциями, не так ли? Я имею в виду, что это в значительной степени определение «функции»: без побочных эффектов. В по крайней мере они должны иметь разные типы возвращаемых данных, например IO int func(); или IO string func();.

Jörg W Mittag 14.01.2009 22:45

@ Jörg - в большинстве основных языков программирования (C / C++, C#, Java и т. д.) Функции обычно имеют побочные эффекты. На самом деле, я бы предположил, что функции с побочными эффектами по крайней мере так же распространены, как и без них.

Michael Burr 15.01.2009 00:20

Я мало что знаю о C или C++, но я точно знаю, что функции в C# и Java не могут иметь побочных эффектов, потому что C# и Java даже не имеют функций имеют. У них есть методы, именно так как может у методов могут быть побочные эффекты.

Jörg W Mittag 15.01.2009 03:32

Йорг, ваша точка зрения на этот счет очень странная. Вы можете это доказать (методы являются функциями с побочными эффектами)?

Johannes Schaub - litb 15.01.2009 16:47

Нет, не могу, потому что я этого не говорил. Пожалуйста, не вкладывай слова мне в рот.

Jörg W Mittag 16.01.2009 00:56

Здесь поздно, но в некоторых контекстах «функция» имеет узкое определение (по сути) «метод без побочных эффектов». Говоря проще, «функция» часто используется взаимозаменяемо с «методом» или «подпрограммой». Йорг либо строг, либо педантичен, в зависимости от вашей точки зрения :)

AwesomeTown 25.02.2009 07:34

Даже позже, некоторые точки зрения могут использовать прилагательные, отличные от строгого или педантичного.

Patrick McDonald 26.06.2012 15:27

Ваш пример правильный. В вашем примере вы вызываете func() без типа возвращаемого значения, поэтому, если типы возвращаемого значения имеют значение, должен быть объявлен void func() { ... }, который вызывается в этом случае. Другими словами: подпись должна быть более строгой, а void также является (пустым) возвращаемым типом. Так что в компиляторе должна выдаваться ошибка, если функция void отсутствует, или выдавать сообщение о том, что вызов неоднозначен.

Matt 21.04.2015 11:35

Большинство статических языков теперь также поддерживают дженерики, которые решат вашу проблему. Как указывалось ранее, без различия параметров невозможно узнать, какой из них вызывать. Так что, если вы хотите сделать это, просто используйте дженерики и закончите.

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

Jay Conrod 14.01.2009 09:58

См. codeproject.com/KB/cpp/returnoverload.aspx для умной стратегии "перегрузки по типу возврата". По сути, вместо определения функции func () вы определяете структуру func, даете ей operator () () и преобразования в каждый соответствующий тип.

j_random_hacker 14.01.2009 10:33

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

Charles Graham 14.01.2009 13:31

В haskell это возможно, даже если у него нет перегрузки функций. Haskell использует классы типов. В программе вы могли видеть:

class Example a where
    example :: Integer -> a

instance Example Integer where  -- example is now implemented for Integer
    example :: Integer -> Integer
    example i = i * 10

Сама по себе перегрузка функций не так популярна. В основном языки, которые я видел с ним, - это C++, возможно, java и / или C#. Во всех динамических языках это сокращение для:

define example:i
  ↑i type route:
    Integer = [↑i & 0xff]
    String = [↑i upper]


def example(i):
    if isinstance(i, int):
        return i & 0xff
    elif isinstance(i, str):
        return i.upper()

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

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

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

  • Динамический набор текста
  • Внутренняя поддержка списков, словарей и строк Юникода
  • Оптимизация (JIT, определение типа, компиляция)
  • Интегрированные инструменты развертывания
  • Поддержка библиотеки
  • Поддержка сообщества и места сбора
  • Богатые стандартные библиотеки
  • Хороший синтаксис
  • Читать цикл печати eval
  • Поддержка рефлексивного программирования

У Haskell есть перегрузка. Классы типов - это языковая функция, которая используется для определения перегруженных функций.

Lii 19.04.2012 20:12

Хорошие ответы! В частности, ответ А.Рекса очень подробен и поучителен. Как он указывает, C++ делает учитывает пользовательские операторы преобразования типов при компиляции lhs = func();(где func на самом деле имя структуры). Мой обходной путь немного другой - не лучше, просто другой (хотя он основан на той же базовой идее).

Тогда как мне нужно было написать в розыске ...

template <typename T> inline T func() { abort(); return T(); }

template <> inline int func()
{ <<special code for int>> }

template <> inline double func()
{ <<special code for double>> }

.. etc, then ..

int x = func(); // ambiguous!
int x = func<int>(); // *also* ambiguous!?  you're just being difficult, g++!

В итоге я получил решение, в котором используется параметризованная структура (с T = типом возврата):

template <typename T>
struct func
{
    operator T()
    { abort(); return T(); } 
};

// explicit specializations for supported types
// (any code that includes this header can add more!)

template <> inline
func<int>::operator int()
{ <<special code for int>> }

template <> inline
func<double>::operator double()
{ <<special code for double>> }

.. etc, then ..

int x = func<int>(); // this is OK!
double d = func<double>(); // also OK :)

Преимущество этого решения заключается в том, что любой код, включающий эти определения шаблонов, может добавлять дополнительные специализации для большего количества типов. Также вы можете выполнить частичную специализацию структуры по мере необходимости. Например, если вам нужна особая обработка типов указателей:

template <typename T>
struct func<T*>
{
    operator T*()
    { <<special handling for T*>> } 
};

Как минус, вы не можете записать int x = func(); с моим решением. Вы должны написать int x = func<int>();. Вы должны явно указать тип возвращаемого значения, а не просить компилятор разобраться в этом, глядя на операторы преобразования типов. Я бы сказал, что "мое" решение и оба А.Рекса входят в Парето-оптимальный фронт способов решения этой дилеммы C++ :)

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

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

procedure(reference string){};
procedure(reference int){};
string blah;
procedure(blah)

Потому что вы не можете сразу повторно использовать "возвращаемые" значения. Вам нужно будет выполнять каждый вызов в одной строке, в отличие от doing(thisVery(deeplyNested(), andOften(butNotAlways()), notReally()), goodCode());.

Adowrath 19.03.2017 00:09

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

public Integer | String f(int choice){
if (choice==1){
return new string();
}else{
return new Integer();
}}

если язык действительно вернул перегрузку, это допустило бы перегрузку параметров, но не дублирование. это решило бы проблему:

main (){
f(x)
}

потому что есть только один f (int choice) на выбор.

В .NET иногда мы используем один параметр, чтобы указать желаемый результат общего результата, а затем выполняем преобразование, чтобы получить то, что мы ожидаем.

C#

public enum FooReturnType{
        IntType,
        StringType,
        WeaType
    }

    class Wea { 
        public override string ToString()
        {
            return "Wea class";
        }
    }

    public static object Foo(FooReturnType type){
        object result = null;
        if (type == FooReturnType.IntType) 
        {
            /*Int related actions*/
            result = 1;
        }
        else if (type == FooReturnType.StringType)
        {
            /*String related actions*/
            result = "Some important text";
        }
        else if (type == FooReturnType.WeaType)
        {
            /*Wea related actions*/
            result = new Wea();
        }
        return result;
    }

    static void Main(string[] args)
    {
        Console.WriteLine("Expecting Int from Foo: " + Foo(FooReturnType.IntType));
        Console.WriteLine("Expecting String from Foo: " + Foo(FooReturnType.StringType));
        Console.WriteLine("Expecting Wea from Foo: " + Foo(FooReturnType.WeaType));
        Console.Read();
    }

Возможно, этот пример тоже может помочь:

C++

    #include <iostream>

enum class FooReturnType{ //Only C++11
    IntType,
    StringType,
    WeaType
}_FooReturnType;

class Wea{
public:
    const char* ToString(){
        return "Wea class";
    }
};

void* Foo(FooReturnType type){
    void* result = 0;
    if (type == FooReturnType::IntType) //Only C++11
    {
        /*Int related actions*/
        result = (void*)1;
    }
    else if (type == FooReturnType::StringType) //Only C++11
    {
        /*String related actions*/
        result = (void*)"Some important text";
    }
    else if (type == FooReturnType::WeaType) //Only C++11
    {
        /*Wea related actions*/
        result = (void*)new Wea();
    }
    return result;
}

int main(int argc, char* argv[])
{
    int intReturn = (int)Foo(FooReturnType::IntType);
    const char* stringReturn = (const char*)Foo(FooReturnType::StringType);
    Wea *someWea = static_cast<Wea*>(Foo(FooReturnType::WeaType));
    std::cout << "Expecting Int from Foo: " << intReturn << std::endl;
    std::cout << "Expecting String from Foo: " << stringReturn << std::endl;
    std::cout << "Expecting Wea from Foo: " << someWea->ToString() << std::endl;
    delete someWea; // Don't leak oil!
    return 0;
}

Это своего рода хакерский прием и может привести к ошибкам во время выполнения, если пользователь неправильно преобразовал результат или если разработчик не сопоставит возвращаемые типы должным образом с перечислением. Я бы рекомендовал использовать подход на основе шаблонов (или общие параметры в C#?), Например, в этот ответ

sleblanc 22.10.2015 23:31

Для записи Октава допускает различный результат в зависимости от того, является ли возвращаемый элемент скалярным по сравнению с массивом.

x = min ([1, 3, 0, 2, 0])
   ⇒  x = 0

[x, ix] = min ([1, 3, 0, 2, 0])
   ⇒  x = 0
      ix = 3 (item index)

См. Также Разложение по сингулярным значениям.

Это немного отличается от C++; Я не знаю, будет ли это считаться перегрузкой напрямую по возвращаемому типу. Это скорее специализация шаблонов, которая действует как.

util.h

#ifndef UTIL_H
#define UTIL_H

#include <string>
#include <sstream>
#include <algorithm>

class util {
public: 
    static int      convertToInt( const std::string& str );
    static unsigned convertToUnsigned( const std::string& str );
    static float    convertToFloat( const std::string& str );
    static double   convertToDouble( const std::string& str );

private:
    util();
    util( const util& c );
    util& operator=( const util& c );

    template<typename T>
    static bool stringToValue( const std::string& str, T* pVal, unsigned numValues );

    template<typename T>
    static T getValue( const std::string& str, std::size_t& remainder );
};

#include "util.inl"

#endif UTIL_H

util.inl

template<typename T>
static bool util::stringToValue( const std::string& str, T* pValue, unsigned numValues ) {
    int numCommas = std::count(str.begin(), str.end(), ',');
    if (numCommas != numValues - 1) {
        return false;
    }

    std::size_t remainder;
    pValue[0] = getValue<T>(str, remainder);

    if (numValues == 1) {
        if (str.size() != remainder) {
            return false;
        }
    }
    else {
        std::size_t offset = remainder;
        if (str.at(offset) != ',') {
            return false;
        }

        unsigned lastIdx = numValues - 1;
        for (unsigned u = 1; u < numValues; ++u) {
            pValue[u] = getValue<T>(str.substr(++offset), remainder);
            offset += remainder;
            if ((u < lastIdx && str.at(offset) != ',') ||
                (u == lastIdx && offset != str.size()))
            {
                return false;
            }
        }
    }
    return true;    
}

util.cpp

#include "util.h"

template<>
int util::getValue( const std::string& str, std::size_t& remainder ) {
    return std::stoi( str, &remainder );
} 

template<>
unsigned util::getValue( const std::string& str, std::size_t& remainder ) {
    return std::stoul( str, &remainder );
}

template<>
float util::getValue( const std::string& str, std::size_t& remainder ) {
    return std::stof( str, &remainder );
}     

template<>   
double util::getValue( const std::string& str, std::size_t& remainder ) {
    return std::stod( str, &remainder );
}

int util::convertToInt( const std::string& str ) {
    int i = 0;
    if ( !stringToValue( str, &i, 1 ) ) {
        std::ostringstream strStream;
        strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to int";
        throw strStream.str();
    }
    return i;
}

unsigned util::convertToUnsigned( const std::string& str ) {
    unsigned u = 0;
    if ( !stringToValue( str, &u, 1 ) ) {
        std::ostringstream strStream;
        strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to unsigned";
        throw strStream.str();
    }
    return u;
}     

float util::convertToFloat(const std::string& str) {
    float f = 0;
    if (!stringToValue(str, &f, 1)) {
        std::ostringstream strStream;
        strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to float";
        throw strStream.str();
    }
    return f;
}

double util::convertToDouble(const std::string& str) {
    float d = 0;
    if (!stringToValue(str, &d, 1)) {
        std::ostringstream strStream;
        strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to double";
        throw strStream.str();
    }
    return d;
}

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

Каждая из функций convertToType вызывает шаблон функции stringToValue(), и если вы посмотрите на детали реализации или алгоритм этого шаблона функции, он вызывает getValue<T>( param, param ) и возвращает обратно тип T и сохраняет его в T*, который передается в функцию stringToValue(). шаблон как один из его параметров.

Кроме чего-то вроде этого; В C++ на самом деле нет механизма для разрешения перегрузки функций по типу возвращаемого значения. Могут существовать другие конструкции или механизмы, о которых я не знаю, которые могут имитировать разрешение по типу возвращаемого значения.

Я думаю, что это пробел в современном определении C++… почему?

int func();
double func();

// example 1. → defined
int i = func();

// example 2. → defined
double d = func();

// example 3. → NOT defined. error
void main() 
{
    func();
}

Почему компилятор C++ не может выдать ошибку в примере «3» и принять код в примере "1 + 2" ??

Да, это то, что они рассматривали в то время для C# (и, возможно, C++). Но хотя ваш код тривиален, после добавления иерархий классов, виртуальных методов, абстрактов и интерфейсов, других перегрузок и, иногда, множественного наследования, очень быстро становится очень сложно решить, какой метод следует разрешить. Дизайнеры решили не идти этим путем, но другие языки решили по-другому на разных уровнях успеха.

Abel 23.12.2019 00:53

если вы хотите перегрузить методы с разными типами возврата, просто добавьте фиктивный параметр со значением по умолчанию, чтобы разрешить выполнение перегрузки, но не забывайте, что тип параметра должен быть другим, поэтому логика перегрузки работает, например, на delphi:

type    
    myclass = class
    public
      function Funct1(dummy: string = EmptyStr): String; overload;
      function Funct1(dummy: Integer = -1): Integer; overload;
    end;

используйте это так

procedure tester;
var yourobject : myclass;
  iValue: integer;
  sValue: string;
begin
  yourobject:= myclass.create;
  iValue:= yourobject.Funct1(); //this will call the func with integer result
  sValue:= yourobject.Funct1(); //this will call the func with string result
end;

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

Abel 23.12.2019 00:47

@Abel то, что вы предлагаете, на самом деле ужасная идея, потому что вся идея связана с этим фиктивным параметром, и он назван так, чтобы прояснить для разработчика, что этот параметр фиктивный и должен быть проигнорирован, также в том случае, если вы не знаю, что фиктивные параметры со значениями по умолчанию используются во многих библиотеках, VCL в delphi и многих IDE, например, в delphi вы можете увидеть это в модуле sysutils в SafeLoadLibrary ...

ZORRO_BLANCO 15.01.2020 13:03

Безусловно, существуют сценарии, в которых полезны фиктивные параметры, например, в лямбдах в операциях map или fold или при реализации интерфейса. Но просто ради перегрузки нет, не соглашусь. В этом нет необходимости, и это шум, без которого программисты могут жить.

Abel 21.01.2020 20:26

Другие вопросы по теме