20

問題

string文字列を表すクラスを実装するとします。operator+次に、 2 つの を連結する を追加し、を実行するときに複数の割り当てを回避するために式テンプレートstringを介して実装することにします。str1 + str2 + ... + strN

オペレーターは次のようになります。

stringbuilder<string, string> operator+(const string &a, const string &b)

stringbuilderはテンプレート クラスであり、オーバーロードoperator+し、暗黙的stringな変換演算子を持ちます。ほとんど標準的な教科書の演習:

template<class T, class U> class stringbuilder;

template<> class stringbuilder<string, string> {
    stringbuilder(const string &a, const string &b) : a(a), b(b) {};
    const string &a;
    const string &b;
    operator string() const;
    // ...
}

// recursive case similar,
// building a stringbuilder<stringbuilder<...>, string>

上記の実装は、誰かがそうする限り完全に機能します

string result = str1 + str2 + ... + strN;

ただし、微妙なバグがあります。結果を正しい型の変数に代入すると、その変数は式を構成するすべての文字列への参照を保持します。これは、たとえば、文字列の 1 つを変更すると結果が変わることを意味します。

void print(string);
string str1 = "foo";
string str2 = "bar";
right_type result = str1 + str2;
str1 = "fie";
print(result); 

式テンプレート内に str1 参照が格納されているため、これによりfiebar が出力されます。ひどくなる:

string f();
right_type result = str1 + f();
print(result); // kaboom

これで、式テンプレートに破棄された値への参照が含まれ、プログラムがすぐにクラッシュします。

今それは何right_typeですか?もちろんstringbuilder<stringbuilder<...>, string>、それは式テンプレートマジックが生成する型です。

では、なぜそのような隠し型を使用するのでしょうか? 実際、明示的に使用することはありませんが、C++11 の auto は使用します!

auto result = str1 + str2 + ... + strN; // guess what's going on here?

質問

要するに、式テンプレートを実装するこの方法 (値をコピーしたり、共有ポインターを使用したりする代わりに安価な参照を保存することによって) は、式テンプレート自体を保存しようとするとすぐに壊れてしまうようです。

したがって、右辺値または左辺値を構築しているかどうかを検出し、右辺値が構築されている (参照を保持する) か、左辺値が構築されている (コピーを作成する) かによって、式テンプレートの異なる実装を提供する方法がかなり必要です。 )。

この状況を処理するための確立された設計パターンはありますか?

調査中にわかった唯一のことは、

  1. this左辺値または右辺値に応じて、メンバー関数をオーバーロードできます。

    class C {
        void f() &; 
        void f() &&; // called on temporaries
    }
    

    ただし、コンストラクターでもそれを行うことはできないようです。

  2. C++ では、実際には「型オーバーロード」を行うことはできません。つまり、型の使用方法(左辺値または右辺値として作成されたインスタンス) に応じて、同じ型の複数の実装を提供することはできません。

4

5 に答える 5

0

Here is another attempt at solving the issue of dangling references. It doesn't solve the issue of references to things that are modified though.

The idea is to store the temporaries into values, but to have references to lvalues (that we can expect to keep living after the ;).

// Temporary => store a copy
// Otherwise, store a reference
template <typename T>
using URefUnlessTemporary_t
= std::conditional_t<std::is_rvalue_reference<T&&>::value
,                    std::decay_t<T>
,                    T&&>
;

template <typename LHS, typename RHS>
struct StringExpression
{
    StringExpression(StringExpression const&) = delete;
    StringExpression(StringExpression     &&) = default;

    constexpr StringExpression(LHS && lhs_, RHS && rhs_)
        : lhs(std::forward<LHS>(lhs_))
        , rhs(std::forward<RHS>(rhs_))
        { }

    explicit operator std::string() const
    {
        auto const len = size(*this);
        std::string res;
        res.reserve(len);
        append(res, *this);
        return res;
    }

    friend constexpr std::size_t size(StringExpression const& se)
    {
        return size(se.lhs) + size(se.rhs);
    }


    friend void append(std::string & s, StringExpression const& se)
    {
        append(s, se.lhs);
        append(s, se.rhs);
    }

    friend std::ostream & operator<<(std::ostream & os, const StringExpression & se)
    { return os << se.lhs << se.rhs; }

private:
    URefUnlessTemporary_t<LHS> lhs;
    URefUnlessTemporary_t<RHS> rhs;
};

template <typename LHS, typename RHS>
StringExpression<LHS&&,RHS&&> operator+(LHS && lhs, RHS && rhs)
{
    return StringExpression<LHS&&,RHS&&>{std::forward<LHS>(lhs), std::forward<RHS>(rhs) };
}

I've no doubt this could be simplified.

int main ()
{
    constexpr static auto c = exp::concatenator{};
    {
        std::cout << "RVREF\n";
        auto r = c + f() + "toto";
        std::cout << r << "\n";
        std::string s (r);
        std::cout << s << "\n";
    }

    {
        std::cout << "\n\nLVREF\n";
        std::string str="lvref";
        auto r = c + str + "toto";
        std::cout << r << "\n";
        std::string s (r);
        std::cout << s << "\n";
    }

    {
        std::cout << "\n\nCLVREF\n";
        std::string const str="clvref";
        auto r = c + str + "toto";
        std::cout << r << "\n";
        std::string s (r);
        std::cout << s << "\n";
    }
}

NB: I don't provide size(), append() nor concatenator, they aren't the points where the difficulties lie.

PS: I've used C++14 only to simplify the type traits.

于 2016-03-30T13:40:36.263 に答える