web-dev-qa-db-ja.com

オプションの関数パラメーター:デフォルトの引数(NULL)を使用するか、関数をオーバーロードしますか?

与えられたベクトルを処理する関数がありますが、与えられていない場合はそのようなベクトル自体を作成することもできます。

関数パラメーターがオプションである場合、このような場合に2つの設計上の選択肢があります。

それをポインターにして、デフォルトでNULLにします:

void foo(int i, std::vector<int>* optional = NULL) {
  if(optional == NULL){
    optional = new std::vector<int>();
    // fill vector with data
  }
  // process vector
}

または、オーバーロードされた名前を持つ2つの関数があり、そのうちの1つは引数を省略します。

void foo(int i) {
   std::vector<int> vec;
   // fill vec with data
   foo(i, vec);
}

void foo(int i, const std::vector<int>& optional) {
  // process vector
}

あるソリューションを他のソリューションよりも優先する理由はありますか?

ベクトルをconst参照にすることができるのは、提供されたときに読み取り専用であり書き込みではないためです。また、インターフェースはきれいに見えます(NULLは単なるハックではありませんか?)。そして、間接的な関数呼び出しから生じるパフォーマンスの違いは、おそらく最適化されます。

しかし、私はしばしばコードの最初の解決策を見ます。プログラマーの怠laz以外に、それを好む説得力のある理由はありますか?

41
Frank

私は間違いなく、オーバーロードされたメソッドの2番目のアプローチを好むでしょう。

最初のアプローチ(optional parameters)は、明確に定義された単一の目的がないため、メソッドの定義を曖昧にします。これにより、コードの複雑さが増加し、コードに精通していない人が理解しにくくなります。

2番目のアプローチ(オーバーロードメソッド)では、各メソッドに明確な目的があります。各メソッドは、well-structuredおよびcohesiveです。いくつかの追加のメモ:

  • 両方のメソッドに複製する必要があるコードがある場合、これを別のメソッドに抽出することができ、各オーバーロードメソッドはこの外部メソッドを呼び出すことができます。
  • さらに一歩進んで、各メソッドに異なる名前を付けて、メソッド間の違いを示します。これにより、コードがより自己文書化されます。
28

私はどちらのアプローチも使用しません。

このコンテキストでは、foo()の目的はベクトルを処理することであるようです。つまり、foo()の仕事はベクターを処理することです。

しかし、foo()の2番目のバージョンでは、ベクターを作成するという2番目のジョブが暗黙的に与えられます。 foo()バージョン1とfoo()バージョン2の間のセマンティクスは同じではありません。

これを行う代わりに、ベクトルを処理するfoo()関数を1つだけと、そのようなことが必要な場合はベクトルを作成する別の関数を用意することを検討します。

例えば:

void foo(int i, const std::vector<int>& optional) {
  // process vector
}

std::vector<int>* makeVector() {
   return new std::vector<int>;
}

明らかにこれらの関数は簡単であり、すべてのmakeVector()を実行するために必要な作業が文字通り単にnewを呼び出すだけである場合、makeVector()関数を使用しても意味がありません。しかし、実際の状況では、これらの関数はここに示されているものよりもはるかに多くのことを行うと確信しており、上記の私のコードはセマンティックデザインへの基本的なアプローチを示しています:。

上記のfoo()関数の設計は、関数のシグネチャ、クラスなどを含むインターフェイスの設計に関して、コードで個人的に使用する別の基本的なアプローチも示しています。つまり、優れたインターフェースは、1)正しく使いやすく、直感的であり、2)間違って使用することが難しいか不可能であるです。 foo()関数の場合、私の設計では、ベクトルはすでに存在し、「準備ができている」必要があると暗黙的に言っています。ポインターの代わりに参照を取るようにfoo()を設計することにより、呼び出し元が既にベクターを持っている必要があることと、すぐに使えるベクターではないものを渡すのに苦労することが直感的です。 。

42
John Dibling

デフォルトのパラメーターとオーバーロードに関する多くの人々の不満は理解していますが、これらの機能が提供する利点については理解が不足しているようです。

デフォルトのパラメーター値:
最初に、プロジェクトの初期設計時に、適切に設計されていれば、デフォルトをほとんどまたはまったく使用しないことを指摘します。ただし、デフォルトの最大の資産が作用するのは、既存のプロジェクトと確立されたAPIです。私は数百万の既存のコード行で構成されるプロジェクトに取り組んでおり、それらすべてを再コーディングする余裕はありません。したがって、追加のパラメーターを必要とする新しい機能を追加する場合。新しいパラメーターにはデフォルトが必要です。そうしないと、プロジェクトを使用するすべてのユーザーが壊れます。個人的にはそれでいいのですが、あなたの会社やあなたの製品/ APIのユーザーは、更新のたびにプロジェクトを再コーディングしなければならないことを感謝します。 単純に、デフォルトは後方互換性に優れています!これは通常、大きなAPIや既存のプロジェクトでデフォルトが表示される理由です。

関数のオーバーライド:関数のオーバーライドの利点は、機能の概念を共有できるが、異なるオプション/パラメーターを使用できることです。ただし、多くの場合、わずかに異なるパラメーターで、まったく異なる機能を提供するために遅延して使用される関数オーバーライドが表示されます。この場合、それぞれに特定の機能に関連する別々の名前の関数が必要です(OPの例と同様)。

これらのc/c ++の機能は優れており、適切に使用するとうまく機能します。これは、ほとんどのプログラミング機能で言えることです。問題を引き起こすのは、虐待/悪用されたときです。

免責事項:
この質問は数年前のものですが、今日(2012年)の検索結果でこれらの答えが出てきたので、将来の読者のためにさらに対処する必要があると感じました。

21
David Ruhmann

C++では参照をNULLにすることはできません。本当に良い解決策は、Nullableテンプレートを使用することです。これはref.isNull()です

ここでこれを使用できます:

template<class T>
class Nullable {
public:
    Nullable() {
        m_set = false;
    }
    explicit
    Nullable(T value) {
        m_value = value;
        m_set = true;
    }
    Nullable(const Nullable &src) {
        m_set = src.m_set;
        if(m_set)
            m_value = src.m_value;
    }
    Nullable & operator =(const Nullable &RHS) {
        m_set = RHS.m_set;
        if(m_set)
            m_value = RHS.m_value;
        return *this;
    }
    bool operator ==(const Nullable &RHS) const {
        if(!m_set && !RHS.m_set)
            return true;
        if(m_set != RHS.m_set)
            return false;
        return m_value == RHS.m_value;
    }
    bool operator !=(const Nullable &RHS) const {
        return !operator==(RHS);
    }

    bool GetSet() const {
        return m_set;
    }

    const T &GetValue() const {
        return m_value;
    }

    T GetValueDefault(const T &defaultValue) const {
        if(m_set)
            return m_value;
        return defaultValue;
    }
    void SetValue(const T &value) {
        m_value = value;
        m_set = true;
    }
    void Clear()
    {
        m_set = false;
    }

private:
    T m_value;
    bool m_set;
};

今、あなたは持つことができます

void foo(int i, Nullable<AnyClass> &optional = Nullable<AnyClass>()) {
   //you can do 
   if(optional.isNull()) {

   }
}
6
Sid Sarasvati

同意します。2つの関数を使用します。基本的に、2つの異なるユースケースがあるので、2つの異なる実装を持つことは理にかなっています。

書くC++コードが多くなればなるほど、パラメータのデフォルトが少なくなります-機能が廃止されたとしても、古いコードの負荷を書き直さなければならないのに、涙を流しません!

5
anon

通常、最初のケースは避けます。これらの2つの機能は、機能が異なることに注意してください。それらの1つは、ベクターにデータを入力します。もう一方はサポートしていません(呼び出し元からのデータを受け入れるだけです)。私は実際に異なることを行う関数に異なる名前を付ける傾向があります。実際、あなたがそれらを書いたとしても、それらは2つの関数です:

  • foo_default(または単にfoo
  • foo_with_values

少なくとも、この区別は長いサームで、そして時々ライブラリ/関数ユーザーのために見つけます。

3
Diego Sevilla

C++では、可能な限り有効なNULLパラメーターを許可しないでください。その理由は、呼び出しサイトのドキュメントを大幅に減らすためです。これは極端に聞こえるかもしれませんが、10〜20個以上のパラメーターを使用するAPIで作業します。パラメーターの半分は有効にNULLにすることができます。結果のコードはほとんど読めません

SomeFunction(NULL, pName, NULL, pDestination);

Const参照を強制するように切り替えると、コードは単純に読みやすくなります。

SomeFunction(
  Location::Hidden(),
  pName,
  SomeOtherValue::Empty(),
  pDestination);
2
JaredPar

私はまさしく「オーバーロード」キャンプにいます。他の人はあなたの実際のコード例についての詳細を追加しましたが、一般的な場合のデフォルトに対するオーバーロードを使用することの利点であると思うものを追加したかったです。

  • どのパラメーターも「デフォルト」にすることができます
  • オーバーライド関数がそのデフォルトに異なる値を使用している場合、問題はありません。
  • 既存の型にデフォルトを持たせるために、既存の型に「hacky」コンストラクターを追加する必要はありません。
  • 出力パラメーターは、ポインターやハッキングされたグローバルオブジェクトを使用せずにデフォルト設定できます。

それぞれにいくつかのコード例を配置するには:

任意のパラメーターをデフォルトに設定できます:

class A {}; class B {}; class C {};

void foo (A const &, B const &, C const &);

inline void foo (A const & a, C const & c)
{
  foo (a, B (), c);    // 'B' defaulted
}

デフォルトに異なる値を持つ関数をオーバーライドする危険はありません:

class A {
public:
  virtual void foo (int i = 0);
};

class B : public A {
public:
  virtual void foo (int i = 100);
};


void bar (A & a)
{
  a.foo ();           // Always uses '0', no matter of dynamic type of 'a'
}

「hacky」コンストラクターを既存の型に追加して、それらをデフォルトに設定する必要はありません:

struct POD {
  int i;
  int j;
};

void foo (POD p);     // Adding default (other than {0, 0})
                      // would require constructor to be added
inline void foo ()
{
  POD p = { 1, 2 };
  foo (p);
}

出力パラメーターは、ポインターやハッキングされたグローバルオブジェクトを使用せずにデフォルト設定できます:

void foo (int i, int & j);  // Default requires global "dummy" 
                            // or 'j' should be pointer.
inline void foo (int i)
{
  int j;
  foo (i, j);
}

ルールの再オーバーロードとデフォルトの唯一の例外は、コンストラクターが現在別のコンストラクターに転送できない場合です。 (C++ 0xはそれを解決すると信じています)。

2
Richard Corden

私も、2番目のものを好みます。 2つの間に大きな違いはありませんが、基本的にはsingfoo(int i)オーバーロードのプライマリメソッドの機能とプライマリオーバーロードは不足の存在を気にせずに完全に機能しますオーバーロードバージョンでは、懸念事項がさらに分離されています。

2
Mehrdad Afshari

3番目のオプションをお勧めします。2つの関数に分割しますが、オーバーロードしません。

過負荷は、本来、あまり使用されません。ユーザーは、2つのオプションを認識し、それらの違いを理解する必要があります。もしそうであれば、ドキュメントまたはコードを確認してどちらが正しいかを確認する必要があります。

パラメーターを受け取る関数と、「createVectorAndFoo」またはそのような名前の関数が1つあります(実際の問​​題で命名が容易になることは明らかです)。

これは「関数の2つの責任」ルールに違反します(長い名前を付けます)が、関数が実際に2つのこと(ベクトルを作成し、fooを作成する)を行う場合はこれが望ましいと思います。

1
Uri

一般に、2機能アプローチを使用するという他者の提案に同意します。ただし、1パラメーター形式を使用するときに作成されるベクトルが常に同じである場合は、代わりに静的にし、代わりにデフォルトのconst&パラメーターを使用して、物事を単純化できます。

// Either at global scope, or (better) inside a class
static vector<int> default_vector = populate_default_vector();

void foo(int i, std::vector<int> const& optional = default_vector) {
    ...
}
1
j_random_hacker

誤ってNULLを渡したのか、それが意図的に行われたのかを判断できないため、最初の方法は貧弱です...それが事故だった場合は、おそらくバグを引き起こした可能性があります。

2番目の方法では、NULLをテスト(アサート、なんでも)し、適切に処理できます。

0
TofuBeer