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

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

Ответ 1

В отличие от других, перегрузка по типу возврата возможна, а -, выполняемая некоторыми современными языками. Обычное возражение состоит в том, что в коде, например

int func();
string func();
int main() { func(); }

вы не можете определить, какой вызов func() вызывается. Это можно решить несколькими способами:

  • Имеет предсказуемый метод определения, какая функция вызывается в такой ситуации.
  • Всякий раз, когда возникает такая ситуация, это ошибка времени компиляции. Однако имеет синтаксис, который позволяет программисту устранить неоднозначность, например. int main() { (string)func(); }.
  • Не имеют побочных эффектов. Если у вас нет побочных эффектов, и вы никогда не используете возвращаемое значение функции, тогда компилятор может избежать вызова функции в первую очередь.

Два из языков, которые я регулярно (ab) используют перегрузку по типу возврата: Perl и Haskell. Позвольте мне описать, что они делают.

В Perl существует фундаментальное различие между скалярным и контекстным списком (и другими, но мы будем делать вид, что их два). Каждая встроенная функция в Perl может делать разные вещи в зависимости от контекста, в котором она вызвана. Например, оператор join заставляет контекст контекста (при соединении вещи), в то время как оператор scalar заставляет скалярный контекст, поэтому сравните:

print join " ", localtime(); # printed "58 11 2 14 0 109 3 13 0" for me right now
print scalar localtime(); # printed "Wed Jan 14 02:12:44 2009" for me right now.

Каждый оператор в Perl что-то делает в скалярном контексте и в контексте списка, и они могут быть разными, как показано. (Это относится не только к случайным операциям типа localtime.Если вы используете массив @a в контексте списка, он возвращает массив, а в скалярном контексте он возвращает количество элементов. Например, print @a prints, а print [email protected] печатает размер.) Кроме того, каждый оператор может заставить контекст, например дополнение + заставляет скалярный контекст. Каждая запись в man perlfunc документирует это. Например, вот часть записи для glob EXPR:

В контексте списка возвращается (возможно пустой) список расширений имени файла на значение EXPR, например, стандартное Unix shell /bin/csh. В скалярный контекст, glob итерации через такие расширения файлов, возвращающие undef, когда список исчерпан.

Теперь, какова связь между списком и скалярным контекстом? Ну, man perlfunc говорит

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

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

Это не просто встроенные функции, которые имеют такое поведение. Любой пользователь может определить такую ​​функцию, используя wantarray, что позволяет различать контекст списка, скаляр и пусто. Так, например, вы можете решить ничего не делать, если вы вызываетесь в пустотном контексте.

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

sub func {
    if( not defined wantarray ) {
        print "void\n";
    } elsif( wantarray ) {
        print "list\n";
    } else {
        print "scalar\n";
    }
}

func(); # prints "void"
() = func(); # prints "list"
0+func(); # prints "scalar"

(Примечание: иногда я могу сказать, что оператор Perl, когда я имею в виду функцию. Это не имеет решающего значения для этого обсуждения.)

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

main = do n <- readLn
          print (sqrt n) -- note that this is aligned below the n, if you care to run this

Этот код считывает число с плавающей запятой из стандартного ввода и печатает его квадратный корень. Но что удивительно в этом? Ну, тип readLn равен readLn :: Read a => IO a. Это означает, что для любого типа, который может быть Read (формально, каждый тип, являющийся экземпляром класса типа Read), readLn может его прочитать. Как Хаскелл знал, что я хотел прочитать номер с плавающей запятой? Ну, тип sqrt равен sqrt :: Floating a => a -> a, что по существу означает, что sqrt может принимать только числа с плавающей запятой в качестве входных данных, поэтому Haskell предположил, что я хотел.

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

main = do n <- readLn
          print n
-- this program results in a compile-time error "Unresolved top-level overloading"

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

main = do n <- readLn
          print (n::Int)
-- this compiles (and does what I want)

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

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

Ada: "Может показаться, что самым простым правилом разрешения перегрузки является использование всего - вся информация из максимально широкого контекста - разрешить перегруженную ссылку.Это правило может быть простым, но это не полезно.Это требует, чтобы человеческий читатель сканировал произвольно большие фрагменты текста и делал произвольно сложные умозаключения (например, (g) выше). Мы считаем, что лучшим правилом является то, что делает явным задачу, которую должен выполнить человеческий читатель или компилятор, и это делает эту задачу естественной для читателя, насколько это возможно".

С++ (подраздел 7.4.1 Bjarne Stroustrup "Язык программирования С++" ): "Типы возврата не учитываются при разрешении перегрузки. Причина заключается в том, чтобы поддерживать разрешение для отдельного оператора или вызова функции контекстно-зависимым. Рассмотрим:

float sqrt(float);
double sqrt(double);

void f(double da, float fla)
{
    float fl = sqrt(da);     // call sqrt(double)
    double d = sqrt(da); // call sqrt(double)
    fl = sqrt(fla);            // call sqrt(float)
    d = sqrt(fla);             // call sqrt(float)
}

Если тип возврата был учтен, было бы невозможно смотреть на вызов sqrt() по отдельности и определять, какая функция была вызвана. "(Обратите внимание, что для сравнения в Haskell нет никаких подразумеваемых конверсии.)

Java (Спецификация языка Java 9.4.1): "Один из унаследованных методов должен быть возвращаемым типом, заменяемым для любого другого унаследованного метода; в противном случае возникает ошибка времени компиляции". (Да, я знаю, что это не дает обоснования. Я уверен, что обоснование дано Гослином на "языке программирования Java". Может быть, у кого-то есть копия? Полагаю, это по сути "принцип наименьшего удивления". ) Однако забавный факт о Java: JVM позволяет перегружать возвращаемое значение! Это используется, например, в Scala, и к нему можно получить доступ непосредственно через Java, а также с помощью внутренних функций.

PS. Как последнее замечание, на самом деле можно перегрузить по возвращаемому значению в С++ с помощью трюка. Свидетель:

struct func {
    operator string() { return "1";}
    operator int() { return 2; }
};

int main( ) {
    int x    = func(); // calls int version
    string y = func(); // calls string version
    double d = func(); // calls int version
    cout << func() << endl; // calls int version
    func(); // calls neither
}

Ответ 2

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

int func();
string func();

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

void main() 
{
    func();
}

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

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

Ответ 3

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

f(g(x))

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

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

Ответ 4

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


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

Причина в том, что разрешение для отдельного оператора или вызова функции не зависит от контекста.

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

И, Господь знает, разрешение перегрузки С++ достаточно сложно, поскольку оно стоит...

Ответ 5

В 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

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

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()

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

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

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

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

Ответ 6

Хорошие ответы! Ответ A.Rex, в частности, очень подробный и поучительный. Как он указывает, С++ рассматривает предоставляемые пользователем операторы преобразования типов при компиляции 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>();. Вы должны явно указать, что такое тип возврата, вместо того, чтобы просить компилятор рассказать об этом, посмотрев на операторы преобразования типов. Я бы сказал, что "мое" решение и A.Rex оба принадлежат парето-оптимальный фронт способов решения этой дилеммы С++:)

Ответ 7

если вы хотите перегрузить методы с разными типами возвращаемых данных, просто добавьте фиктивный параметр со значением по умолчанию, чтобы разрешить выполнение перегрузки, но не забывайте, что тип параметра должен быть другим, поэтому следующая логика перегрузки работает, например, на 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;

Ответ 8

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

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

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

Ответ 9

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

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

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

main (){
f(x)
}

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

Ответ 10

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

С#

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();
    }

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

С++

    #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;
}

Ответ 11

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

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

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

Cf также Разложение сингулярного значения.

Ответ 12

Этот немного отличается для 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++ на самом деле не имеет механизма для разрешения перегрузки функций по типу возвращаемого значения. Могут быть другие конструкции или механизмы, о которых я не знаю, которые могут имитировать разрешение по типу возвращаемого значения.

Ответ 13

Я думаю, что это GAP в современном определении 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"??

Ответ 14

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