web-dev-qa-db-ja.com

戻り値の型による関数のオーバーロード?

なぜ主流の静的型付け言語が戻り型による関数/メソッドのオーバーロードをサポートしないのですか?私はそれを考えることはできません。パラメータタイプごとにオーバーロードをサポートするのと同じくらい有用または合理的と思われます。どうしてそんなに人気がないのでしょうか?

245
dsimcha

他の人が言っていることとは反対に、戻り値のタイプによるオーバーロードは可能ですおよびisいくつかの現代言語で行われます。通常の反対は、次のようなコードで

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

どのfunc()が呼び出されているかわかりません。これはいくつかの方法で解決できます。

  1. そのような状況でどの関数が呼び出されるかを判断する予測可能な方法を用意してください。
  2. このような状況が発生するたびに、それはコンパイル時エラーです。ただし、プログラマーが曖昧さをなくすことができる構文があります。 int main() { (string)func(); }
  3. 副作用はありません。副作用がなく、関数の戻り値を使用しない場合、コンパイラは最初から関数を呼び出すことを回避できます。

私が定期的に使用する2つの言語( ab )戻り値のタイプごとにオーバーロードを使用します:PerlおよびHaskell。それらが何をするのか説明させてください。

Perlでは、スカラーリストコンテキスト(およびその他、 2つのふりをします)。 Perlのすべての組み込み関数は、呼び出されるcontextに応じて異なることを実行できます。たとえば、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はサイズを出力します。)さらに、すべての演算子はforceコンテキスト、たとえばさらに+はスカラーコンテキストを強制します。 man perlfuncのすべてのエントリはこれを文書化します。たとえば、glob EXPRのエントリの一部は次のとおりです。

リストコンテキストでは、標準のUnixシェル/bin/cshが行うようなEXPRの値でのファイル名展開の(空の場合もある)リストを返します。スカラーコンテキストでは、globはそのようなファイル名の展開を反復し、リストが使い果たされるとundefを返します。

さて、リストとスカラーコンテキストの関係は何ですか?さて、man perlfuncは言う

次の重要なルールを覚えておいてください。リストコンテキストの式の動作をスカラーコンテキストの動作に関連付けたり、その逆を行うルールはありません。 2つのまったく異なることを行う可能性があります。各演算子と関数は、スカラーコンテキストで返すのに最適な値の種類を決定します。一部の演算子は、リストコンテキストで返されるリストの長さを返します。一部の演算子は、リストの最初の値を返します。一部の演算子は、リストの最後の値を返します。一部の演算子は、成功した操作の数を返します。一般に、一貫性が必要な場合を除き、それらは必要なことを行います。

そのため、単一の関数を持つという単純な問題ではなく、最後に単純な変換を行います。実際、そのためにlocaltimeの例を選択しました。

この動作を行うのはビルトインだけではありません。すべてのユーザーは、wantarrayを使用してこのような関数を定義できます。これにより、リスト、スカラー、およびvoidコンテキストを区別できます。したがって、たとえば、voidコンテキストで呼び出されている場合は何もしないことを決定できます。

さて、これはtrueではないという不満を言うかもしれません。関数は1つしかなく、呼び出されたコンテキストに通知され、その情報に基づいて動作するため、戻り値によるオーバーロードではありません。ただし、これは明らかに同等です(また、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が読み取れることを意味します。 Haskellは、浮動小数点数を読みたいと思ったことをどのようにして知りましたか? sqrtのタイプはsqrt :: Floating a => a -> aです。これは、本質的にsqrtが入力として浮動小数点数のみを受け入れることを意味するため、Haskellは私が望んでいたものを推測しました。

Haskellが私が欲しいものを推測できない場合はどうなりますか?さて、いくつかの可能性があります。戻り値をまったく使用しない場合、Haskellは最初から関数を呼び出しません。ただし、I 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++(Bjarne Stroustrupの「C++プログラミング言語」のサブセクション7.4.1):「戻り値の型は、オーバーロード解決では考慮されません。理由は、個々の演算子または関数呼び出しの解決をコンテキストに依存しないためです。

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 Language Specification 9.4.1 ):「継承されたメソッドの1つは、他の継承されたメソッドの代替可能な戻り型でなければなりません。そうしないと、コンパイル時エラーが発生します。」 (はい、私はこれが理論的根拠を与えていないことを知っています。理論的根拠は「Javaプログラミング言語」でゴスリングによって与えられたと確信しています。しかし、Javaについての楽しい事実:JVM allows戻り値によるオーバーロード!これは、例えば 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
}
513
A. Rex

関数が戻り値の型によってオーバーロードされ、これら2つのオーバーロードがあった場合

int func();
string func();

コンパイラが、このような呼び出しを見たときに、これら2つの関数のどちらを呼び出すべきかを判断する方法はありません。

void main() 
{
    func();
}

このため、言語設計者は戻り値のオーバーロードをしばしば許可しません。

ただし、一部の言語(MSILなど)では、doは戻り型によるオーバーロードを許可します。もちろんこれらも上記の困難に直面していますが、回避策がありますので、ドキュメントを参照する必要があります。

35

そのような言語では、以下をどのように解決しますか:

f(g(x))

fにオーバーロードがあった場合void f(int)およびvoid f(string)およびgにオーバーロードがあった場合int g(int)およびstring g(int)?何らかの曖昧さ回避ツールが必要になります。

あなたがこれを必要とするかもしれない状況は、関数の新しい名前を選択することでより良く役立つと思います。

27
Greg Hewgill

C++固有の 非常によく似た別の質問からの回答 (重複?)を盗むには:


Stroustrup(私は他のC++アーキテクトからの入力を前提としています)がオーバーロード解決を「コンテキスト非依存」にしたかったため、関数の戻り値の型はオーバーロード解決で機能しません。 「C++プログラミング言語、第3版」の7.4.1-「オーバーロードと戻り値の型」を参照してください。

理由は、コンテキストに依存しない個々の演算子または関数呼び出しの解決を維持するためです。

彼らは、結果がどのように使用されたかではなく、オーバーロードがどのように呼び出されたかにのみ基づいていることを望んでいました(使用された場合)。実際、結果を使用せずに多くの関数が呼び出されるか、結果がより大きな式の一部として使用されます。彼らがこれを決定したときに私が確信した1つの要因は、戻り値の型が解決の一部である場合、複雑なルールで解決する必要があるか、コンパイラにスローする必要があるオーバーロードされた関数への呼び出しが多くなることでした呼び出しがあいまいだったというエラー。

そして、C++のオーバーロード解決は、それが立っているので十分に複雑です...

18
Michael Burr

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

したがって、あまり意味はありません。ほとんどの人は、使用する場所ごとに1行削除するのに言語が役立つかどうかに興味はありません。

パターンマッチングは、関数のオーバーロードにやや似ており、時々同様に機能すると思います。ただし、少数のプログラムでのみ有用であり、ほとんどの言語で実装するのが難しいため、一般的ではありません。

次のように、言語に実装するための実装しやすい他の優れた機能が無限にあります。

  • 動的型付け
  • リスト、辞書、およびユニコード文字列の内部サポート
  • 最適化(JIT、型推論、コンパイル)
  • 統合展開ツール
  • 図書館サポート
  • コミュニティのサポートと集会場所
  • 豊富な標準ライブラリ
  • 良い構文
  • Eval印刷ループを読み取ります
  • リフレクティブプログラミングのサポート
5
Cheery

良い答えです!特にA.Rexの回答は非常に詳細で有益です。彼が指摘するように、C++doesは、lhs = func();( funcは実際には構造体の名前です)。私の回避策は少し異なります-良くはありませんが、違います(同じ基本的な考え方に基づいています)。

一方、wantedを書く必要がありました...

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++ジレンマに取り組む方法の パレート最適フロント に属していると言えます。

3
Adam McKee

異なる戻り値の型でメソッドをオーバーロードする場合は、デフォルト値のダミーパラメーターを追加して、オーバーロードの実行を許可しますが、パラメーター型が異なることを忘れないでください。デルファイの例:

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;
1
ZORRO_BLANCO

すでに示したように、戻り値の型によってのみ異なる関数のあいまいな呼び出しはあいまいさをもたらします。あいまいさは、コードの欠陥を引き起こします。欠陥のあるコードは避けなければなりません。

あいまいさの試みによって引き起こされる複雑さは、これが良いハックではないことを示しています。知的演習とは別に、参照パラメーターを使用して手順を使用しないでください。

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

レコードの場合、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も 特異値分解 です。

0
YvesgereY

これは、C++ではわずかに異なります。戻り値の型によって直接オーバーロードと見なされるかどうかはわかりません。のように機能するのは、テンプレートの特殊化です。

til.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

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

til.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()関数テンプレートにそのパラメーターの1つとして渡されます。

このようなもの以外; C++には、戻り値の型による関数のオーバーロード解決を行うメカニズムが実際にはありません。返り値のタイプによって解決をシミュレートできる他の構造体またはメカニズムが、私が知らないかもしれません。

0
Francis Cugler

.NETでは、一般的な結果から目的の出力を示すために1つのパラメーターを使用してから、期待どおりの結果を得るために変換を行うことがあります。

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

このオーバーロード機能は、少し異なる方法で見れば管理が難しくありません。以下を考慮してください。

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

言語がオーバーロードを返した場合、パラメーターのオーバーロードは許可されますが、重複は許可されません。これにより、次の問題が解決されます。

main (){
f(x)
}

選択できるf(int choice)が1つしかないためです。

0
paulon0n