Перегрузка функции по типу возврата? - PullRequest
243 голосов
/ 14 января 2009

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

Ответы [ 14 ]

514 голосов
/ 14 января 2009

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

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

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

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

Два языка, которые я регулярно ( 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 печатает элементы, в то время как print 0+@a печатает размер.) Кроме того, каждый оператор может заставить контекст, например сложение + вызывает скалярный контекст. Каждая запись в man perlfunc документирует это. Например, вот часть записи для glob EXPR:

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

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

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

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

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

Теперь вы можете жаловаться, что это не true перегрузка возвращаемым значением, потому что у вас есть только одна функция, которая сообщает контекст, в котором она вызывается, и затем воздействует на эту информацию. Однако это явно эквивалентно (и аналогично тому, как 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 просто не будет вызывать функцию. Однако, если я do использую возвращаемое значение, то 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) выше). Мы считаем, что лучшим правилом является правило, которое делает задачу явным читателем-человеком или читателем. должен выполнить компилятор, и это делает эту задачу настолько естественной для читателя, насколько это возможно. "

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

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. В заключение отметим, что на самом деле можно перегрузить возвращаемым значением в C ++ с помощью хитрости. Свидетель:

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
}
35 голосов
/ 14 января 2009

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

int func();
string func();

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

void main() 
{
    func();
}

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

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

27 голосов
/ 14 января 2009

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

f(g(x))

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

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

18 голосов
/ 14 января 2009

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


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

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

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

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

5 голосов
/ 14 января 2009

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

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

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

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

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

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

1 голос
/ 30 мая 2018

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

Я думаю, что это 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" ??

0 голосов
/ 15 марта 2018

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

0 голосов
/ 09 ноября 2015

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

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

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

Cf также Разложение по единственному значению .

...