Przeciążenie funkcji według typu zwrotu?


252

Dlaczego więcej popularnych języków statycznych nie obsługuje przeciążania funkcji / metod według typów zwracanych? Nie mogę wymyślić nic takiego. Wydaje się nie mniej użyteczne lub uzasadnione niż wspieranie przeciążenia według typu parametru. Dlaczego jest o wiele mniej popularny?


Odpowiedzi:


523

W przeciwieństwie do tego, co mówią inni, przeciążenie według typu zwrotu jest możliwe i jest wykonywane przez niektóre współczesne języki. Zazwyczaj sprzeciw jest taki jak w kodzie

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

nie możesz powiedzieć, która func()jest wywoływana. Można to rozwiązać na kilka sposobów:

  1. Mieć przewidywalną metodę określania, która funkcja jest wywoływana w takiej sytuacji.
  2. Ilekroć taka sytuacja ma miejsce, jest to błąd czasu kompilacji. Jednak mają składnię, która pozwala programiście ujednoznacznić, np int main() { (string)func(); }.
  3. Nie ma skutków ubocznych. Jeśli nie masz efektów ubocznych i nigdy nie używasz zwracanej wartości funkcji, kompilator może w ogóle uniknąć wywołania funkcji w pierwszej kolejności.

Dwa z języków, w których regularnie ( ab ) używam przeciążenia według typu zwrotu: Perl i Haskell . Pozwól mi opisać, co robią.

W Perlu istnieje podstawowa różnica między skalarem a kontekstem listy (i innymi, ale będziemy udawać, że są dwa). Każda wbudowana funkcja w Perlu może robić różne rzeczy w zależności od kontekstu, w którym jest wywoływana. Na przykład joinoperator wymusza kontekst listy (na łączonej rzeczy), podczas gdy scalaroperator wymusza kontekst skalarny, więc porównaj:

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.

Każdy operator w Perlu robi coś w kontekście skalarnym i coś w kontekście listy, i mogą być różne, jak pokazano. (Nie dotyczy to tylko losowych operatorów, takich jak localtime. Jeśli używasz tablicy @aw kontekście listy, zwraca tablicę, podczas gdy w kontekście skalarnym zwraca liczbę elementów. Na przykład print @adrukuje elementy, podczas gdy print 0+@adrukuje rozmiar. ) Ponadto każdy operator może wymusić kontekst, np. Dodanie +wymusza kontekst skalarny. Każdy wpis w man perlfunctym dokumentuje. Na przykład tutaj jest część wpisu dotyczącego glob EXPR:

W kontekście listy zwraca (ewentualnie pustą) listę rozszerzeń nazw plików na wartość, jaką zrobiłaby EXPRstandardowa powłoka uniksowa /bin/csh. W kontekście skalarnym glob iteruje przez takie rozszerzenia nazw plików, zwracając undef po wyczerpaniu listy.

Jaki jest związek między listą a kontekstem skalarnym? Cóż, man perlfuncmówi

Zapamiętaj następującą ważną zasadę: Nie ma reguły, która wiązałaby zachowanie wyrażenia w kontekście listy z jego zachowaniem w kontekście skalarnym lub odwrotnie. Może to zrobić dwie zupełnie różne rzeczy. Każdy operator i funkcja decyduje, jaki rodzaj wartości najlepiej byłoby zwrócić w kontekście skalarnym. Niektórzy operatorzy zwracają długość listy, która zostałaby zwrócona w kontekście listy. Niektórzy operatorzy zwracają pierwszą wartość z listy. Niektórzy operatorzy zwracają ostatnią wartość z listy. Niektórzy operatorzy zwracają liczbę udanych operacji. Ogólnie rzecz biorąc, robią to, co chcesz, chyba że chcesz spójności.

więc nie jest to prosta kwestia posiadania jednej funkcji, a następnie wykonujesz prostą konwersję na końcu. Z tego powodu wybrałem localtimeprzykład.

Nie tylko wbudowane mają takie zachowanie. Każdy użytkownik może zdefiniować taką funkcję za pomocą wantarray, która pozwala rozróżnić kontekst listowy, skalarny i void. Na przykład możesz zdecydować, że nic nie zrobisz, jeśli zostaniesz wezwany w pustym kontekście.

Teraz możesz narzekać, że nie jest to prawdziwe przeciążenie wartością zwracaną, ponieważ masz tylko jedną funkcję, która jest informowana o kontekście, w którym jest wywoływana, a następnie działa na podstawie tych informacji. Jest to jednak wyraźnie równoważne (i analogiczne do tego, w jaki sposób Perl nie pozwala na zwykłe przeciążanie dosłownie, ale funkcja może po prostu zbadać swoje argumenty). Co więcej, dobrze rozwiązuje niejednoznaczną sytuację wspomnianą na początku tej odpowiedzi. Perl nie narzeka, że ​​nie wie, którą metodę wywołać; to po prostu to nazywa. Wystarczy dowiedzieć się, w jakim kontekście została wywołana funkcja, co jest zawsze możliwe:

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"

(Uwaga: czasami mogę powiedzieć operator Perla, gdy mam na myśli funkcję. Nie jest to kluczowe w tej dyskusji).

Haskell przyjmuje inne podejście, mianowicie nie wywoływać skutków ubocznych. Ma również silny system typów, dzięki czemu możesz pisać kod w następujący sposób:

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

Ten kod odczytuje liczbę zmiennoprzecinkową ze standardowego wejścia i wypisuje pierwiastek kwadratowy. Ale co jest w tym zaskakującego? Cóż, typ readLnjest readLn :: Read a => IO a. Oznacza to, że dla każdego typu, który może być Read(formalnie, każdy typ, który jest instancją Readklasy typu), readLnmoże go odczytać. Skąd Haskell wiedział, że chcę odczytać liczbę zmiennoprzecinkową? No cóż, typ sqrtjest sqrt :: Floating a => a -> a, co w gruncie rzeczy oznacza, że sqrtakceptuje tylko liczby zmiennoprzecinkowe jako dane wejściowe, więc Haskell wywnioskował, czego chciałem.

Co się stanie, gdy Haskell nie będzie mógł wywnioskować, czego chcę? Cóż, jest kilka możliwości. Jeśli w ogóle nie użyję wartości zwracanej, Haskell po prostu nie wywoła tej funkcji. Jednak gdybym zrobić użyj wartości zwracanej, a następnie Haskell będzie skarżyć, że nie można ustalić typ:

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

Mogę rozwiązać niejednoznaczność, określając żądany typ:

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

Tak czy inaczej, cała ta dyskusja oznacza, że ​​przeciążenie wartością zwracaną jest możliwe i odbywa się, co odpowiada części twojego pytania.

Inną częścią twojego pytania jest to, dlaczego więcej języków tego nie robi. Pozwolę innym odpowiedzieć na to pytanie. Jednak kilka uwag: główną przyczyną jest prawdopodobnie to, że szansa na zamieszanie jest tutaj naprawdę większa niż w przypadku przeciążenia według typu argumentu. Możesz także spojrzeć na uzasadnienia z poszczególnych języków:

Ada : „Może się wydawać, że najprostszą regułą rozwiązywania przeciążenia jest wykorzystanie wszystkiego - wszystkich informacji z jak najszerszego kontekstu - do rozwiązania przeciążonego odwołania. Reguła może być prosta, ale nie jest pomocna. Wymaga od czytelnika skanować dowolnie duże fragmenty tekstu i dokonywać dowolnie złożonych wniosków (takich jak (g) powyżej). Uważamy, że lepsza reguła to taka, która wyraźnie określa zadanie, które musi wykonać czytelnik lub kompilator, i to sprawia, że ​​to zadanie tak naturalne dla ludzkiego czytelnika, jak to możliwe. ”

C ++ (podsekcja 7.4.1 „Język programowania C ++” Bjarne Stroustrupa): „Typy zwrotów nie są uwzględniane w rozwiązywaniu przeciążeń. Powodem jest zachowanie rozdzielczości dla pojedynczego operatora lub wywołania funkcji niezależnie od kontekstu. Rozważ:

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

Gdyby wzięto pod uwagę typ zwracany, nie byłoby już możliwe oddzielne sprawdzenie wywołania sqrt()i określenie, która funkcja została wywołana. ”(Należy zauważyć, dla porównania, że ​​w Haskell nie ma żadnych niejawnych konwersji).

Java ( Java Language Specification 9.4.1 ): „Jedna z odziedziczonych metod musi zastępować typem zwracanym dla każdej innej odziedziczonej metody, w przeciwnym razie wystąpi błąd kompilacji”. (Tak, wiem, że to nie daje uzasadnienia. Jestem pewien, że uzasadnienie podane jest przez Goslinga w „Java Programming Language”. Może ktoś ma kopię? Założę się, że to w zasadzie „zasada najmniejszego zaskoczenia”. ) Jednak fajny fakt o Javie: JVM pozwala na przeładowanie przez wartość zwracaną! Jest to używane na przykład w Scali i można uzyskać do niego bezpośredni dostęp również przez Javę , grając z elementami wewnętrznymi.

PS. Na koniec, w rzeczywistości możliwe jest przeciążenie przez zwrócenie wartości w C ++ za pomocą lewy. Świadek:

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
}

Świetny post, ale możesz chcieć wyjaśnić, co to jest czytanie (String -> coś).
Thomas Eding

C ++ pozwala także na przeładowanie przez const / not const zwracaną wartość. stackoverflow.com/questions/251159/…
geon

3
W przypadku ostatniej sztuczki polegającej na przeciążeniu operatorów przymusu linia „cout” działa czasami, ale prawie każda zmiana, którą wprowadzam w kodzie, powoduje, że powoduje ona „niejednoznaczne przeciążenie dla operatora <<”.
Steve

1
Preferowanym przeze mnie podejściem byłoby wymaganie, aby jedno przeciążenie było oznaczone jako „preferowane”; kompilator zaczynałby od wiązania przy użyciu tylko preferowanych przeciążeń, a następnie ustalał, czy jakieś niepreferencyjne przeciążenia byłyby poprawą. Między innymi, załóżmy typów Fooi Barobsługę dwukierunkową konwersję i zastosowań metody typu Foowewnętrznie, ale powraca typu Bar. Jeśli taka metoda zostanie wywołana przez kod, który natychmiast zmusi wynik do wpisania Foo, użycie Bartypu return może działać, ale ta metoda Foobyłaby lepsza. BTW, chciałbym również zobaczyć środki, za pomocą których ...
supercat 15.12.12

... metoda może wyznaczyć, jaki typ powinien zostać użyty w konstrukcji var someVar = someMethod();(lub też wyznaczyć, że jego zwrot nie powinien być używany w taki sposób). Na przykład rodzina typów, która implementuje płynny interfejs, może var thing2 = thing1.WithX(3).WithY(5).WithZ(9);odnieść korzyści z posiadania wersji modyfikowalnych i niezmiennych, więc WithX(3)kopiowałaby thing1do obiektu zmiennego, mutowała X i zwracała ten zmienny obiekt; WithY(5)mutuje Y i zwraca ten sam obiekt; podobnie `WithZ (9). Następnie przydział przekształciłby się w niezmienny typ.
supercat,

37

Jeśli funkcje były przeciążone przez typ zwracany i wystąpiły te dwa przeciążenia

int func();
string func();

kompilator nie może dowiedzieć się, które z tych dwóch funkcji wywołać po zobaczeniu takiego połączenia

void main() 
{
    func();
}

Z tego powodu projektanci języków często nie dopuszczają do przeciążania wartości zwrotu.

Niektóre języki (takie jak MSIL), ale nie pozwalają przeciążenie przez typ zwracany. Oni również napotykają powyższe trudności, ale mają obejścia, dla których będziesz musiał zapoznać się z ich dokumentacją.


4
Drobna sprzeczka (twoja odpowiedź daje bardzo jasne i zrozumiałe uzasadnienie): to nie tak, że nie ma mowy; po prostu sposoby byłyby niezręczne i bardziej bolesne, niż większość ludzi chciałaby. Na przykład w C ++ przeciążenie prawdopodobnie można by rozwiązać, stosując brzydką składnię rzutowania.
Michael Burr

2
@ Jörg W Mittag: Nie widzisz, jakie funkcje pełnią. Mogą łatwo wywoływać różne działania niepożądane.
A. Rex

2
@ Jörg - w większości popularnych języków programowania (C / C ++, C #, Java itp.) Funkcje zwykle mają skutki uboczne. W rzeczywistości sądzę, że funkcje z efektami ubocznymi są co najmniej tak powszechne jak te bez nich.
Michael Burr

6
Skaczę tu późno, ale w niektórych kontekstach „funkcja” ma wąską definicję (zasadniczo) „metody bez skutków ubocznych”. Bardziej potocznie „funkcja” jest często używana zamiennie z „metodą” lub „podprogramem”. Jorg jest albo rygorystyczny, albo pedantyczny, w zależności od twojego punktu widzenia :)
AwesomeTown

3
Wskakując jeszcze później, niektóre punkty widzenia mogą używać przymiotników innych niż rygorystyczne lub pedantyczne
Patrick McDonald

27

W takim języku, w jaki sposób rozwiązałbyś następujące kwestie:

f(g(x))

jeśli fmiał przeciążeń void f(int)i void f(string)i gmiał przeciążeń int g(int)i string g(int)? Potrzebowałbyś pewnego rodzaju disambiguator.

Myślę, że sytuacjom, w których możesz tego potrzebować, lepiej byłoby obsłużyć, wybierając nową nazwę dla funkcji.


2
Regularny rodzaj przeciążenia może również powodować niejasności. Myślę, że normalnie są one rozwiązywane przez zliczenie wymaganej liczby rzutów, ale to nie zawsze działa.
Jay Conrod

1
tak, standardowe konwersje są uszeregowane według dopasowania ścisłego, promocji i konwersji: void f (int); void f (długi); fa'); wywołuje f (int), ponieważ jest to tylko promocja, a konwersja na długi jest konwersją. void f (float); void f (krótki); f (10); wymagałoby konwersji dla obu: połączenie jest niejednoznaczne.
Johannes Schaub - litb

Jeśli język ma leniwą ocenę, nie stanowi to większego problemu.
jdd 30.10.10

Upvote, interakcja przeciążenia typu parametru i przeciążenia typu powrotu nie została uwzględniona w poście Rexa. Bardzo dobra uwaga.
Joseph Garvin

1
Gdybym projektował język, moja reguła byłaby taka, że ​​dla każdej przeciążonej funkcji każda sygnatura parametru musi mieć jeden typ zwracany wyznaczony jako domyślny; kompilator zacząłby od założenia, że ​​każde wywołanie funkcji użyje domyślnego typu. Po wykonaniu tej czynności jednak w każdej sytuacji, w której wartość zwracana przez funkcję jest natychmiast rzutowana lub wymuszana na czymś innym, kompilator sprawdza, czy nie ma przeciążenia, którego sygnatura parametru jest identyczna, ale którego typ zwracany jest lepszy (lub ewentualnie nieważny) . Prawdopodobnie narzuciłbym też zasadę „override-one - override-all” dla takich przeciążeń.
supercat

19

Aby ukraść odpowiedź specyficzną dla C ++ z innego bardzo podobnego pytania (dupe?):


Typy zwracanych funkcji nie wchodzą w grę w rozdzielczości przeciążenia tylko dlatego, że Stroustrup (zakładam, że z danych wejściowych innych architektów C ++) chciał, aby rozdzielczość przeciążenia była „niezależna od kontekstu”. Patrz 7.4.1 - „Przeciążenie i typ zwrotu” z „C ++ Programming Language, Third Edition”.

Powodem jest zachowanie rozdzielczości dla pojedynczego operatora lub wywołania funkcji niezależnie od kontekstu.

Chcieli, aby opierało się to tylko na tym, jak wywołano przeciążenie - a nie na tym, jak wykorzystano wynik (jeśli w ogóle został wykorzystany). Rzeczywiście, wiele funkcji jest wywoływanych bez użycia wyniku lub wynik byłby użyty jako część większego wyrażenia. Jednym z czynników, który jestem pewien, że wszedł do gry, gdy zdecydowali, że jest to, że jeśli typ zwracany był częścią rozdzielczości, byłoby wiele wywołań przeciążonych funkcji, które musiałyby zostać rozwiązane przy użyciu skomplikowanych reguł lub musiałyby mieć rzut kompilatora błąd polegający na tym, że połączenie było niejednoznaczne.

I, Pan wie, rozdzielczość przeciążenia C ++ jest wystarczająco złożona, ponieważ stoi ...


5

W haskell jest to możliwe, nawet jeśli nie ma przeciążenia funkcji. Haskell używa klas typów. W programie można było zobaczyć:

class Example a where
    example :: Integer -> a

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

Samo przeciążanie funkcji nie jest tak popularne. Najczęściej używane języki to C ++, być może Java i / lub C #. We wszystkich dynamicznych językach jest to skrót od:

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

Dlatego nie ma to większego sensu. Większość ludzi nie jest zainteresowana tym, czy język może pomóc upuścić jedną linię w każdym miejscu, w którym go używasz.

Dopasowywanie wzorców jest nieco podobne do przeciążania funkcji i myślę, że czasami działają podobnie. Nie jest to jednak powszechne, ponieważ jest przydatne tylko dla kilku programów i jest trudne do wdrożenia w większości języków.

Widzisz, istnieje nieskończenie wiele innych łatwiejszych do zaimplementowania funkcji do zaimplementowania w języku, w tym:

  • Pisanie dynamiczne
  • Wewnętrzna obsługa list, słowników i ciągów Unicode
  • Optymalizacje (JIT, wnioskowanie typu, kompilacja)
  • Zintegrowane narzędzia do wdrażania
  • Obsługa bibliotek
  • Wsparcie społeczności i miejsca spotkań
  • Bogate standardowe biblioteki
  • Dobra składnia
  • Przeczytaj eval print loop
  • Obsługa programowania refleksyjnego

3
Haskell ma przeciążenie. Klasy typów to funkcja języka używana do definiowania przeciążonych funkcji.
Lii

2

Dobre odpowiedzi! Odpowiedź A.Rex jest bardzo szczegółowa i pouczająca. Jak sam zaznacza, C ++ ma rozważyć operatorów typu konwersji dostarczone przez użytkownika podczas kompilacji lhs = func(); (gdzie func jest naprawdę nazwa struktury) . Moje obejście jest trochę inne - nie lepsze, po prostu inne (chociaż opiera się na tym samym podstawowym pomyśle).

Podczas gdy chciałem napisać ...

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++!

Skończyło się na rozwiązaniu, które wykorzystuje sparametryzowaną strukturę (z T = typ zwracany):

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

Zaletą tego rozwiązania jest to, że każdy kod, który zawiera te definicje szablonów, może dodać więcej specjalizacji dla większej liczby typów. W razie potrzeby możesz także wykonywać częściowe specjalizacje struktury. Na przykład, jeśli chcesz specjalnej obsługi typów wskaźników:

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

Negatywnie nie możesz pisać za int x = func();pomocą mojego rozwiązania. Musisz pisać int x = func<int>();. Musisz wyraźnie powiedzieć, jaki jest typ zwracany, zamiast prosić kompilator o wyszukanie go, patrząc na operatory konwersji typów. Powiedziałbym, że zarówno „moje” rozwiązanie, jak i A.Rex należą do pareto-optymalnego przodu sposobów rozwiązania tego dylematu C ++ :)


1

jeśli chcesz przeciążać metody różnymi typami zwracanymi, po prostu dodaj fikcyjny parametr z wartością domyślną, aby umożliwić wykonanie przeciążenia, ale nie zapominaj, że typ parametru powinien być inny, więc logika przeciążania działa dalej to np. na delphi:

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

użyj tego w ten sposób

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;

To okropny pomysł. Nie wprowadzaj fałszywych parametrów, to duży zapach kodu. Zamiast tego wybierz inne nazwy lub typ zwrotu, który może zachowywać się podobnie, lub jest dyskryminowanym związkiem lub czymś takim.
Abel

@Abel to, co sugerujesz, jest w rzeczywistości okropnym pomysłem, ponieważ cały pomysł dotyczy tego parametru pozornego i tak się nazywa, aby dać programistom do zrozumienia, że ​​ten parametr jest pozorny i należy go zignorować, również w przypadku nie wiem, parametry manekina z wartościami domyślnymi są używane w wielu bibliotekach, VCL w delphi i wielu IDE, np. w delphi można to zobaczyć w jednostce sysutils w SafeLoadLibrary ...
ZORRO_BLANCO

Z pewnością istnieją scenariusze, w których przydatne są parametry pozorne, na przykład w lambdach w operacjach mapowania lub składania lub podczas implementacji interfejsu. Ale ze względu na powstanie przeciążenia nie, błagam się nie zgodzić. Nie ma potrzeby i to hałas, bez którego programiści mogą żyć.
Abel

0

Jak już pokazano - niejednoznaczne wywołania funkcji, która różni się tylko typem zwracanym, wprowadzają niejednoznaczność. Niejednoznaczność powoduje wadliwy kod. Wadliwego kodu należy unikać.

Złożoność wynikająca z próby niejednoznaczności pokazuje, że nie jest to dobry hack. Oprócz ćwiczenia intelektualnego - dlaczego nie zastosować procedur z parametrami odniesienia.

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

Ponieważ nie można łatwo ponownie natychmiast użyć wartości „return”. Musiałbyś wykonać każde połączenie na jednej linii, w przeciwieństwie dodoing(thisVery(deeplyNested(), andOften(butNotAlways()), notReally()), goodCode());
Adowrath

0

ta funkcja przeciążania nie jest trudna do zarządzania, jeśli spojrzysz na nią w nieco inny sposób. rozważ następujące,

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

jeśli język zwróci przeciążenie, pozwoli to na przeładowanie parametrów, ale nie na powielanie. rozwiązałoby to problem:

main (){
f(x)
}

ponieważ jest tylko jeden f (int wybór) do wyboru.


0

W .NET czasami używamy jednego parametru do wskazania pożądanego wyniku ogólnego wyniku, a następnie dokonaliśmy konwersji, aby uzyskać to, czego oczekujemy.

DO#

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

Może ten przykład też może pomóc:

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
Jest to trochę hackerskie i może prowadzić do błędów w czasie wykonywania, jeśli użytkownik nie rzuci poprawnie wyniku lub programista nie dopasuje poprawnie typów zwracanych do wyliczenia.
Poleciłbym użycie

0

Dla przypomnienia , Octave zezwala na inny wynik w zależności od tego, czy zwracany element jest skalarny w porównaniu do tablicy.

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

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

Por. Także rozkład wartości pojedynczej .


0

Ten jest nieco inny dla C ++; Nie wiem, czy byłoby to uważane za przeciążenie bezpośrednio przez typ zwrotu. Jest to bardziej specjalizacja szablonowa, która działa w sposób.

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

W tym przykładzie nie do końca używana jest funkcja przeciążenia funkcji według typu zwracanego, jednak ta nie obiektowa klasa c ++ używa specjalizacji szablonu do symulacji rozdzielczości przeciążenia funkcji według typu zwracanego za pomocą prywatnej metody statycznej.

Każda z convertToTypefunkcji wywołuje szablon funkcji, stringToValue()a jeśli spojrzysz na szczegóły implementacji lub algorytm tego szablonu funkcji, wywołuje getValue<T>( param, param )on, a następnie zwraca typ Ti zapamiętuje go jako T*przekazany do stringToValue()szablonu funkcji jako jeden z jego parametrów .

Inne niż coś takiego; C ++ tak naprawdę nie ma mechanizmu, który ma rozdzielczość przeciążania funkcji według typu zwracanego. Mogą istnieć inne konstrukcje lub mechanizmy, o których nie wiem, które mogą symulować rozdzielczość według typu zwracanego.


-1

Myślę, że jest to luka w nowoczesnej definicji C ++… dlaczego?

int func();
double func();

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

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

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

Dlaczego kompilator C ++ nie może zgłosić błędu w przykładzie „3” i zaakceptować kod w przykładzie „1 + 2”?


Tak, właśnie to rozważali w tym czasie dla C # (a może C ++). Ale chociaż kod jest trywialny, po dodaniu hierarchii klas, metod wirtualnych, streszczeń i interfejsów, innych przeciążeń, a niekiedy wielokrotnego dziedziczenia, bardzo szybko staje się bardzo skomplikowane podejmowanie decyzji, którą metodę należy rozwiązać. Projektanci nie wybierają tej drogi, ale inne języki decydują inaczej na różnych poziomach sukcesu.
Abel,

-2

Większość języków statycznych obsługuje teraz również generyczne, które rozwiązałyby twój problem. Jak wspomniano wcześniej, bez różnic parametrów nie ma sposobu, aby sprawdzić, który z nich należy wywołać. Więc jeśli chcesz to zrobić, po prostu użyj ogólnych i nazwij to dzień.


Nie to samo. Jak poradziłbyś sobie z funkcją, która tłumaczy dane wejściowe na liczbę całkowitą, liczbę zmiennoprzecinkową, wartość boolową lub cokolwiek innego w zależności od tego, jak używany jest typ zwracany? Nie można go uogólnić, ponieważ dla każdego potrzebujesz specjalnego przypadku.
Jay Conrod

Zobacz codeproject.com/KB/cpp/returnoverload.aspx, aby uzyskać sprytną strategię „przeciążania po typie zwrotu”. Zasadniczo zamiast definiować funkcję func (), definiuje się strukturę func, nadajemy operatorowi () () i konwersje do każdego odpowiedniego typu.
j_random_hacker

Jay, definiujesz typ zwracany, gdy wywołujesz funkcję. Jeśli inpus są różne, nie ma żadnego problemu. Jeśli są takie same, możesz mieć ogólną wersję, która może mieć logikę opartą na typie za pomocą GetType ().
Charles Graham
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.