非会員、非友人を優先しますか?

std::string を実行するメンバー関数の数

C++17 の時点で、正しく数えたと仮定すると、答えは 153 です。

百五十三。

それはたくさん .Herb Sutter が指摘したように、これらのメンバーのほとんどは、パフォーマンスを損なうことなく非メンバーとして簡単に実装できます。

また、これらは C++ コーディング標準の古いガイドラインに従って非メンバーとして実装する必要があります:非メンバー、非フレンドを優先します。可能であれば自由な関数を作成し、メンバーではありません。

しかし、そのアドバイスは本当にどの程度真実なのでしょうか?

非会員、非友人を優先

Scott Meyers は、項目 18 (完全で最小限のクラス インターフェイスを目指して努力する) と項目 19 (メンバー関数、非メンバー関数、およびフレンド関数を区別する) で優れた点を指摘し、Guru of the Week #84 やその他多くの点を指摘しました。ので、ここでそれらすべてを詳細に繰り返すつもりはありません。

要点は次のとおりです。大きなクラスは、維持する作業が多くなり、理解が難しくなり、単一責任の原則に違反し、より緊密な結合につながります。特定のクラス。30 ~ 30 を参照してください。 - std::string の関数を見つける 24 は std::string_view に移植されます 、おそらくまったく同じ実装です。

したがって、一般的な考え方は次のとおりです。関数が非メンバーにできる*場合は、非メンバーにします。canの定義 次のように決定されます (C++ コーディング標準に従って):

メンバーにする必要がある場合は、メンバーにする必要があります (operator= のような特殊な演算子 )、非メンバー関数 (引数の型変換など) である必要がある場合は、非メンバーにする必要があります。それ以外の場合、決定は単に関数が 効率的に 実装できるかどうかである必要があります。 メンバー関数のみを使用します。さらに、friend よりもメンバー関数を優先する必要があります 関数。

ただし、メンバー関数の代わりに非メンバー関数を記述すると問題があります。これは実装の詳細ではありませんが、呼び出し構文が異なるため、ユーザーにとって明らかな変更です。

これはさまざまな問題を引き起こします:

1.非メンバー関数は連鎖を厄介にする

シンタックス シュガーの問題から始めて、上に向かって作業を進めていきましょう。非メンバー関数がある場合、連鎖は厄介です。

私の ts::optional 実装を考えてみましょう。とりわけ、2 つのメンバー関数 value_or() を提供します。 と map() .value_or() オプションが空の場合は、保存された値またはフォールバック値のいずれかを返し、map() 格納されたオプションに関数を適用し、変換された値を含むオプションを返すか、元のオプションが空の場合は変更された型の空のオプションを返します。

has_value() を使用して、両方の機能をパフォーマンスのオーバーヘッドなしで簡単に実装できます。 および value() メンバー関数:

template <typename T, typename U>
T value_or(const ts::optional<T>& optional, U&& fallback)
{
 return optional.has_value() ? optional.value() : std::forward<U>(fallback);
}

template <typename T, typename Func>
auto map(const ts::optional<T>& optional, Func f)
-> ts::optional<decltype(f(optional.value()))>
{
 return optional.has_value() ? ts::make_optional(f(optional.value())) : ts::nullopt;
}

ただし、 value_or() のそれらの定義 特に map() それらの目的を完全に無効にします。オプションの値のシンプルで安全な処理を可能にします:

ts::optional<id> try_get_id();
T lookup(const id& i);
…
auto value = try_get_id()
 .map(&lookup) // get an optional<T>
 .map(&calculate_value) // get an optional value
 .value_or(42); // get the value or 42

この投稿は、そのコードの美しさを納得させようとしているわけではありません。ただ受け入れて、非メンバーの同等のものと比較してください:

auto value = value_or(map(map(try_get_id(), &lookup), &calculate_value), 42);

これを読むのはほとんど不可能です。

大量の一時ファイルを作成する必要があります:

auto id = try_get_id();
auto t = map(id, &lookup);
auto maybe_value = map(t, &calculate_value);
auto value = value_or(maybe_value, 42);

または、マップをまったく使用しない:

auto value = 42;
if (auto id = try_get_id(); id.has_value())
{
 auto t = lookup(id.value());
 value = calculate_value(t);
}

そのため、メンバー関数を作成する必要がありました。簡単なチェーンが必要でした。

これも理想的ではないことに注意してください:私のバリアントには map() もあります 非常によく似た実装を使用します。非メンバーの場合、map() を提供する汎用機能を作成できたはずです。 しかし、私は実装経験よりもユーザー経験を選ばなければなりませんでした.

2.非メンバー関数は実装の詳細を公開します

シンプルな片方向リストの実装を考えてみましょう。メモリ フットプリントを最小限に抑えるために、リストのサイズを別の変数に格納しません。代わりに、最初のノードへのポインタのみを格納します。

size() を実装したい場合 提供されたイテレータ インターフェイスに関しては簡単に作成できるので、非メンバ関数にします:

template <typename T>
std::size_t size(const my_list<T>& list)
{
 return std::distance(list.begin(), list.end());
}

ただし、サイズをメンバー変数として保存することを選択した場合は、メンバー関数にします:

template <typename T>
std::size_t my_list<T>::size() const
{
 return size_;
}

リストの実装は、特に size() かどうかにかかわらず、ユーザー インターフェイスに直接影響を与えました。 メンバー関数または非メンバー関数になります。

この特定のケースでは、これは良いことであると主張することができます.サイズを格納するリストには、格納しないリストとは異なるアプリケーションがあります.しかし、これには一般的なコードの問題があります:

3.非メンバー関数は、汎用コードで問題を引き起こす可能性があります

size() のコンテナが 1 つある場合 は非メンバー関数であるため、メンバー size() を想定するすべての汎用コードで使用することはできません function.そして、すべての STL コンテナーにはメンバー size() があるため

さらに:

4.メンバー関数は、ジェネリック コードで問題を引き起こす可能性があります

一般的なコンテキストでコレクションのサイズを取得したいとします:

template <typename Container>
void foo(const Container& cont)
{
 auto size = cont.size();
 …
}

すべての STL コンテナーにメンバー関数があるため、メンバー関数を呼び出します。ただし、これは次のコードで問題を引き起こします:

int array[] = {…};
foo(array);

配列に .size() がありません;メンバー関数を持つことはできません!代わりに、機能する非メンバー サイズがあると仮定して、それを呼び出す必要があります。

両方の問題の解決策は、ラッパーを導入して代わりに呼び出すことです:

template <typename T>
auto do_get_size_impl(int, const T& obj) -> decltype(obj.size())
{
 return obj.size();
}

template <typename T>
std::size_t do_get_size_impl(char, const T& obj)
{
 using my_array_size_namespace::size;
 return size(obj);
}

template <typename T>
std::size_t do_get_size(const T& obj)
{
 return do_get_size_impl(0, obj);
}

これは、新しい std::size が行うことと似ています。しかし、これは多くのボイラープレートです。

メンバー以外の友達を優先しますか?

したがって、関数メンバーをいつ作成するかを決定する最初のアルゴリズムは、構文を認識する必要があるため機能しません。代わりに、改訂されたアルゴリズムは次のようになります。

また、汎用アルゴリズムのガイドライン:

ただし、これは適切なガイドラインではありません。

しかし、潜在的な解決策があります:

統一された呼び出し構文

一般的な問題は、メンバー関数呼び出しの構文が非メンバー関数呼び出しの構文と異なることですが、これはまったく問題ではありません!メンバー関数と非メンバー関数の間にぎこちない違いがあることには何のメリットもありません。これは何の情報も公開しません。

連鎖させたい場合や、特別な引数が 1 つある場合は、メンバー関数の構文の方が適切です。その他の状況では、非メンバー関数の構文の方が適切です。2 つの構文形式を簡単に切り替えることができれば、すばらしいことです。

これが、提案された統一呼び出し構文の背後にある考え方です。それはまさにそれを可能にしますが、今のところ受け入れられていません.

N4474 を言い換えると、さまざまなアプローチがあります:

<オール>
  • x.f(y) を一般化する f(x, y) を呼び出す 、一致するメンバー関数がない場合。
  • f(x, y) を一般化する x.f(y) に電話する 、一致する無料関数がない場合。
  • 1.と2.の両方を行う
  • x.f(y) と書く場合 、すべてのメンバー関数とフリー関数を考慮し、オーバーロードの解決を使用して、どれを呼び出す必要があるかを決定します。f(x, y) の場合はその逆 .
  • x.f(y) と書く場合 または f(x, y) 最初にメンバー関数を探し、次にフリー関数を探します。
  • それぞれのアプローチにはそれぞれ長所と短所があるため、1 つを選択することは困難です。私が知る限り、現在のアプローチは 3 ですが、正確なステータスはわかりません。

    いつの日かこれが C++ に組み込まれることを心から願っています.なぜなら、今の状況は厄介だからです.

    結論

    非メンバーの非フレンドを優先することは合理的なガイドラインですが、残念ながら普遍的に適用できるわけではありません。

    しかし、統一された呼び出し構文が得られれば、ガイドラインは C++ コーディング標準のガイドラインになる可能性があります:

    そして、各ユーザーはそれを呼び出す方法を決定できます。これが実際に必要なことです。

    付録:完璧な世界で

    メンバー関数が間違っていたと思います.

    議論された問題に加えて、 const の後に奇妙な定義構文があります。 と && 少し異なるルールがあります。

    さらに、これらは 3 つの個別の機能で解決できる問題を解決します:

      <リ>

      特定の関数をフレンドとしてマークせずに、クラスのプライベート データにアクセスできるようにします。完璧な世界では、もちろんモジュールがあります。 - これは、モジュール内のすべての関数、または Rust の impl のようなものと同じくらい簡単です。 ブロック:そこにあるすべての関数は、クラスのプライベート データ (AFAIK) にアクセスできます。

      <リ>

      フリー関数のポリモーフィック動作を許可します。1 つまたは複数をマークできます。 - virtual の引数 派生型の場合はオーバーライドできます。または、他のメカニズムを使用してください。

      <リ>

      1 つの引数のメンバーへの自動アクセスを許可します。これは、any に名前を付けるメカニズムを導入することで解決できます。 パラメータ this 、名前検索はそのメンバーを考慮します。これにより、面倒なオブジェクト接頭辞が解決されます。

    これらを使用すると、メンバー関数が提供するすべてのものを使用できますが、よりシンプルでクリーンです。ユニバーサル関数呼び出し構文により、実装者ではなく呼び出し元が、状況に応じて関数呼び出しがどのように見えるかが可能になります。

    残念ながら、これはおそらく C++ では不可能なので、関数呼び出しの構文が統一されることを期待するのが最善です.