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

252

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

dsimcha
источник

Ответы:

523

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

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. В скалярном контексте 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может его прочитать. Как Хаскелл узнал, что я хочу прочитать число с плавающей запятой? Ну, тип sqrtis sqrt :: Floating a => a -> a, что по сути означает, что он sqrtможет принимать только числа с плавающей запятой в качестве входных данных, и поэтому 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)

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

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

Ада : «Может показаться, что самое простое правило разрешения перегрузки - это использовать все - всю информацию из максимально широкого контекста - для разрешения перегруженной ссылки. Это правило может быть простым, но оно не полезно. Оно требует читателя-человека. сканировать произвольно большие фрагменты текста и делать произвольно сложные выводы (такие как (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
}
А. Рекс
источник
Отличный пост, но вы можете уточнить, что такое чтение (String -> что-то).
Томас Эдинг
C ++ также позволяет вам перегружать константным / неконстантным возвращаемым значением. stackoverflow.com/questions/251159/…
Geon
3
Для вашего последнего трюка с перегрузкой операторов принуждения иногда работает строка «cout», но почти любое изменение, которое я делаю в коде, приводит к «неоднозначной перегрузке для оператора <<».
Стив
1
Подход, который я бы предпочел, состоял бы в том, чтобы одна перегрузка была отмечена как «предпочтительная»; компилятор будет начинать с привязки с использованием только предпочтительных перегрузок, а затем определять, будут ли какие-либо нежелательные перегрузки улучшаться. Среди прочего, предположим , что типы Fooи Barподдержка двунаправленного преобразования и способ применения типа Fooвнутренне , но тип возвращается Bar. Если такой метод вызывается кодом, который немедленно приведет результат к типу Foo, использование Barвозвращаемого типа может сработать, но Fooтот будет лучше. Кстати, я также хотел бы увидеть средства, с помощью которых ...
суперкат
... метод может указать, какой тип должен использоваться в подобной конструкции var someVar = someMethod();(или указать, что его возвращение не должно использоваться таким образом). Например, семейство типов, которое реализует интерфейс Fluent, может выиграть от наличия изменяемых и неизменяемых версий, поэтому var thing2 = thing1.WithX(3).WithY(5).WithZ(9);имело бы WithX(3)копию thing1в изменяемый объект, изменяющее X и возвращающее этот изменяемый объект; WithY(5)мутирует Y и возвращает тот же объект; аналогично `WithZ (9). Тогда назначение будет преобразовано в неизменный тип.
суперкат
37

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

int func();
string func();

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

void main() 
{
    func();
}

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

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

Фредерик Дурак
источник
4
Небольшой спор (ваш ответ дает очень четкое и понятное обоснование): это не значит, что нет никакого пути; просто пути были бы неуклюжими и более болезненными, чем хотелось бы большинству людей. Например, в C ++ перегрузка, вероятно, была бы разрешима, используя некоторый уродливый синтаксис приведения.
Майкл Берр
2
@ Йорг W Mittag: Вы не видите, что делают функции. Они могут легко иметь различные побочные эффекты.
А. Рекс
2
@ Jörg - в большинстве основных языков программирования (C / C ++, C #, Java и т. Д.) Функции обычно имеют побочные эффекты. На самом деле, я предполагаю, что функции с побочными эффектами, по крайней мере, так же распространены, как и без них.
Майкл Берр
6
Прыжок в конце здесь, но в некоторых контекстах «функция» имеет узкое определение (по существу) «метод без побочных эффектов». Говоря языком, «функция» часто используется взаимозаменяемо с «методом» или «подпрограммой». Йорг строгий или педантичный, в зависимости от вашей точки зрения :)
AwesomeTown
3
Прыгая даже позже, некоторые точки зрения могут использовать прилагательные, отличные от строгих или педантичных
Патрик Макдональд
27

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

f(g(x))

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

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

Грег Хьюгилл
источник
2
Обычная перегрузка также может привести к неоднозначности. Я думаю, что они обычно решаются путем подсчета необходимого количества приведений, но это не всегда работает.
Джей Конрод
1
да, стандартные конверсии ранжируются по точному совпадению, продвижению и конверсии: void f (int); пустота f (длинная); е ( 'а'); вызывает f (int), потому что это всего лишь продвижение, а преобразование в long - это преобразование. void f (float); пустота f (короткая); F (10); потребует преобразования для обоих: вызов неоднозначен.
Йоханнес Шауб -
Если язык имеет ленивую оценку, это не такая большая проблема.
JDD
Upvote, взаимодействие перегрузки типов параметров и возвращаемых типов не рассматривается в посте Рекса. Очень хороший момент.
Джозеф Гарвин
1
Если бы я разрабатывал язык, мое правило состояло бы в том, что для любой перегруженной функции каждая сигнатура параметра должна иметь один тип возвращаемого значения, обозначенный по умолчанию; компилятор будет начинать с предположения, что каждый вызов функции будет использовать тип по умолчанию. Однако после того, как это было сделано, в каждой ситуации, когда возвращаемое значение функции было немедленно приведено или приведено к чему-то другому, компилятор проверял бы перегрузку, сигнатура параметра которой идентична, но чей тип возврата является лучшим соответствием (или, возможно, пустым) , Я бы, вероятно, также наложил бы правило "override-one - override-all" для таких перегрузок.
Суперкат
19

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


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

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

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

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

Майкл Берр
источник
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

Функция перегрузки сама по себе не так популярна. Большинство языков, которые я видел, это 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, вывод типа, компиляция)
  • Интегрированные инструменты развертывания
  • Поддержка библиотеки
  • Общественная поддержка и места сбора
  • Богатые стандартные библиотеки
  • Хороший синтаксис
  • Читать цикл печати
  • Поддержка рефлексивного программирования
бодрый
источник
3
У Haskell есть перегрузка. Классы типов - это языковая функция, которая используется для определения перегруженных функций.
Лии
2

Хорошие ответы! В частности, ответ А.Рекса очень подробный и поучительный. Как он указывает, 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

если вы хотите перегрузить методы с различными типами возвращаемых данных, просто добавьте фиктивный параметр со значением по умолчанию, чтобы разрешить выполнение перегрузки, но не забывайте, что тип параметра должен отличаться, поэтому следующая логика перегрузки работает, например, на 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;
ZORRO_BLANCO
источник
Это ужасная идея. Не вводите фиктивные параметры, это большой запах кода. Вместо этого выберите другие имена или выберите тип возвращаемого значения, который может действовать как, или является дискриминационным объединением или чем-то еще.
Авель
@ Абель, что вы предлагаете, на самом деле является ужасной идеей, потому что вся идея связана с этим фиктивным параметром, и он назван так, чтобы дать понять разработчику, что этот параметр является фиктивным и должен игнорироваться, в том случае, если вы Не знаю, что фиктивные параметры со значениями по умолчанию используются во многих библиотеках, VCL в Delphi и во многих IDE, например, в Delphi вы можете увидеть это в модуле sysutils в SafeLoadLibrary ...
ZORRO_BLANCO
Конечно, есть сценарии, где фиктивные параметры полезны, например, в лямбдах в операциях отображения или сгиба или при реализации интерфейса. Но просто ради создания перегрузки, я прошу не согласиться. Там нет нужды, и это шум, без которого программисты могут жить без.
Авель
0

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

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

procedure(reference string){};
procedure(reference int){};
string blah;
procedure(blah)
Codeless
источник
Потому что вы не можете легко повторно использовать возвращаемые значения немедленно. Вы должны будете делать каждый звонок на отдельной линии, в отличие отdoing(thisVery(deeplyNested(), andOften(butNotAlways()), notReally()), goodCode());
Adowrath
0

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

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

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

main (){
f(x)
}

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

paulon0n
источник
0

В .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;
}
Факундо
источник
1
Это своего рода хакерство, и оно может привести к ошибкам во время выполнения, если пользователь неверно приведёт результат или если разработчик неправильно сопоставит возвращаемые типы с перечислением. Я бы порекомендовал использовать основанный на шаблонах подход (или общие параметры в C #?), Такие как в этом ответе
sleblanc
0

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

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

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

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

YvesgereY
источник
0

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

Фрэнсис Куглер
источник
-1

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

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

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

Чарльз Грэм
источник
Не то же самое. Как бы вы справились с функцией, которая переводит входные данные в целое число, число с плавающей точкой, bool или что-то еще в зависимости от того, как используется тип возвращаемого значения? Это не может быть обобщено, так как вам нужен особый случай для каждого.
Джей Конрод
См. Codeproject.com/KB/cpp/returnoverload.aspx для умной стратегии «перегрузки по типу возврата». По сути, вместо определения функции func () вы определяете struct func, присваиваете ей оператор () () и выполняете преобразования для каждого соответствующего типа.
j_random_hacker
Джей, вы определяете тип возвращаемого значения при вызове функции. Если входные данные различны, то проблем нет вообще. Если есть то же самое, у вас может быть общая версия, которая может иметь некоторую логику, основанную на типе, используя GetType ().
Чарльз Грэм