2012-03-08 16 views
4

スニペットを考えて、a、b、c、dが空でない文字列であるとしましょう。次いでstd :: stringと複数の連結

std::string a, b, c, d; 
    d = a + b + c; 

それら3つのstd::stringインスタンスの合計を計算する場合、標準ライブラリ実装が第1の一時std::stringオブジェクトを作成し、その内部バッファにabの連結バッファをコピーすることは、一時的な文字列の間で同様の動作を行いますc

この動作の代わりに、operator+(std::string, std::string)を定義してstd::string_helperを返すように仲間のプログラマーが強調していました。

このオブジェクトの非常に重要な役割は、実際の連結をstd::stringにキャストされた瞬間に延期することです。明らかに、operator+(std::string_helper, std::string)は、同じヘルパーを返すように定義されています。これは、実行する追加の連結があるということを「覚えておく」でしょう。

このような動作は、n-1個の一時オブジェクトを作成し、バッファを割り当て、コピーするなどのCPUコストを節約します。任意の欠点または制限。

+1

最も明白な欠点は複雑さです。 – PlasmaHH

+0

C++ 11では、値はrvalue参照のおかげで再利用できます。 – avakar

+4

@PlasmaHH:複雑さはユーザーから隠されているので、特に悪くはありません。主な欠点は、 'std :: string'からの暗黙の変換に依存する既存のコードを壊す、暗黙的なユーザー定義の型変換を導入することです。 –

答えて

6

どうしてそんなにうまくいかないのですか?

なぜ私はそれがもともとそのように設計された理由について推測することができます。おそらく文字列ライブラリのデザイナーは単にそれを考えなかったでしょう。おそらく彼らは、余分な型変換(下記参照)が、ある状況ではその動作をあまりにも驚くほどにするかもしれないと考えていたでしょう。それは最も古くからあるC++ライブラリの1つであり、われわれが当然受け持っている多くの知恵は過去数十年間に存在しなかっただけです。

なぜそれがそのように動作するように変更されていないかについては、余分なユーザー定義の型変換を追加することで、既存のコードを破損する可能性があります。暗黙的な変換には、最大で1つのユーザー定義変換のみが関係します。これは13.3.3.1.2/1、C++ 11で指定される:

ユーザ定義の変換シーケンスは、第2のスタンダード続いユーザ定義変換続く初期基準変換配列からなります変換シーケンス。

は、次のことを考えてみましょう:

struct thingy { 
    thingy(std::string); 
}; 

void f(thingy); 

f(some_string + another_string); 

some_string + another_stringの種類がstd::stringであればこのコードは結構です。これは暗黙的に変換コンストラクタを介してthingyに変換できます。しかし、operator+の定義を変更して別のタイプを指定すると、2回の変換(string_helper〜〜thingy)が必要になるため、コンパイルに失敗します。

文字列作成の速度が重要な場合は、+=との連結のような別の方法を使用する必要があります。 Matthieuの答えによると、C++ 11は非効率性を別の方法で修正しているので、心配しないでください。

+2

このテクニックはC++(1990年頃)を学んでいた時からよく知られていたので、オリジナルのデザイナーがそれを聞いていなかったという理由は疑う。おそらく、 'std :: string'で期待される典型的な用途ではデザインが悪いと感じました。 –

+0

@JamesKanze:十分な公正。私の知識は90年代半ばにしか戻りませんので、私は以前の開発について推測することができます。 –

+0

@Mike:しかし、 'std :: string_helper'は暗黙のキャスト演算子を' std :: string'に持っていますが、コードをコンパイルするのに十分ではありませんか? – qdii

2

このようなもののように私に聞こえる音:std::stringstream

あなただけが+の代わりに<<を持っています。ただstd::string::operator +が存在するので、最も効率的なオプションにはなりません。

0

私はあなたが+=を使用している場合、それは少し速くなると思う:

d += a; 
d += b; 
d += c; 

それは、単にこの

d.append(a).append(b).append(c); //same as above: i.e using '+=' 3 times. 
+0

これで、右辺値の参照ができたので、それは高速ではありません。 –

+0

@MooingDuck:正確にはそれほど高速ではありませんか? – Nawaz

+0

あなたの投稿のコードのいずれかは、OP内のコードよりも12バイト少ない1つのmemcpyでなければなりません。 –

0

を一時的objects.Orを作成していないとしてそれは、より速くする必要があります個々の+連結の文字列を行わず、特にループでそれをしない主な理由は、O( n )の複雑さを持つことです。

O( N)複雑で、合理的な代替は、次にあなたが&hellipのような効率的かつ明確なものを書くことができ

template< class Char > 
class ConversionToString 
{ 
public: 
    // Visual C++ 10.0 has some DLL linking problem with other types: 
    CPP_STATIC_ASSERT((
     std::is_same< Char, char >::value || std::is_same< Char, wchar_t >::value 
     )); 

    typedef std::basic_string<Char>   String; 
    typedef std::basic_ostringstream<Char> OutStringStream; 

    // Just a default implementation, not particularly efficient. 
    template< class Type > 
    static String from(Type const& v) 
    { 
     OutStringStream stream; 
     stream << v; 
     return stream.str(); 
    } 

    static String const& from(String const& s) 
    { 
     return s; 
    } 
}; 


template< class Char, class RawChar = Char > 
class StringBuilder; 


template< class Char, class RawChar > 
class StringBuilder 
{ 
private: 
    typedef std::basic_string<Char>  String; 
    typedef std::basic_string<RawChar> RawString; 
    RawString s_; 

    template< class Type > 
    static RawString fastStringFrom(Type const& v) 
    { 
     return ConversionToString<RawChar>::from(v); 
    } 

    static RawChar const* fastStringFrom(RawChar const* s) 
    { 
     assert(s != 0); 
     return s; 
    } 

    static RawChar const* fastStringFrom(Char const* s) 
    { 
     assert(s != 0); 
     CPP_STATIC_ASSERT(sizeof(RawChar) == sizeof(Char)); 
     return reinterpret_cast< RawChar const* >(s); 
    } 

public: 
    enum ToString { toString }; 
    enum ToPointer { toPointer }; 

    String const& str() const    { return reinterpret_cast< String const& >(s_); } 
    operator String const&() const   { return str(); } 
    String const& operator<<(ToString) { return str(); } 

    RawChar const*  ptr() const   { return s_.c_str(); } 
    operator RawChar const*() const  { return ptr(); } 
    RawChar const* operator<<(ToPointer) { return ptr(); } 

    template< class Type > 
    StringBuilder& operator<<(Type const& v) 
    { 
     s_ += fastStringFrom(v); 
     return *this; 
    } 
}; 

template< class Char > 
class StringBuilder< Char, Char > 
{ 
private: 
    typedef std::basic_string<Char> String; 
    String s_; 

    template< class Type > 
    static String fastStringFrom(Type const& v) 
    { 
     return ConversionToString<Char>::from(v); 
    } 

    static Char const* fastStringFrom(Char const* s) 
    { 
     assert(s != 0); 
     return s; 
    } 

public: 
    enum ToString { toString }; 
    enum ToPointer { toPointer }; 

    String const& str() const    { return s_; } 
    operator String const&() const   { return str(); } 
    String const& operator<<(ToString) { return str(); } 

    Char const*  ptr() const    { return s_.c_str(); } 
    operator Char const*() const   { return ptr(); } 
    Char const* operator<<(ToPointer)  { return ptr(); } 

    template< class Type > 
    StringBuilder& operator<<(Type const& v) 
    { 
     s_ += fastStringFrom(v); 
     return *this; 
    } 
}; 

namespace narrow { 
    typedef StringBuilder<char>  S; 
} // namespace narrow 

namespace wide { 
    typedef StringBuilder<wchar_t> S; 
} // namespace wide 

のように、単純な文字列ビルダを使用することです。

using narrow::S; 

std::string a = S() << "The answer is " << 6*7; 
foo(S() << "Hi, " << username << "!"); 
4

です。

C++ 03では、そこでは多少の非効率性があります(途中で文字列インターンを使用するのでJavaとC#に匹敵します)。これは、次のように使用して軽減できます。

d = std::string("") += a += b +=c; 

実際には...慣用ではありません。

C++ 11では、operator+は右辺値参照のためにオーバーロードされています。意味あること:あなたが得ることができるように(ほぼ)として効率的である

d.assign(std::move(operator+(a, b).append(c))); 

d = a + b + c; 

することに変換されます。

C++ 11バージョンで残っている唯一の非効率性は、メモリが最初に一度予約されていないことです。つまり、新しい割り当てごとに最大2回の再割り当てとコピーが行われる可能性があります。それでも、追加は償却O(1)なので、CがBよりかなり長い場合を除いて、最悪の場合、再割り当て+コピーが1回発生します。もちろん、私たちはここでPODコピーを話しています(memcpyコール)。

+0

+1:これは興味深いことです。 「追加は償却されたO(1)」とはどういう意味ですか? – qdii

+0

@qdii:償却O(1)は、アルゴリズムの複雑さ解析で使用される用語です。それは常にO(1)ではないことを意味します(時にはトリガーに再割り当て+メモリのコピーをトリガーするため)。*平均ではO(1)です。これは、一般に、基礎となるバッファが指数関数的に増加することによって行われるため、物事が成長するにつれて、再配分が必要でなくなることは少なくなります。たとえば、ストレージを増やすたびにストレージを倍増させることは、適切な戦略です。 –

6

明らかな答え:標準では許可されていないためです。またいくつかの場合には、追加のユーザ定義の変換を導入することによって影響コード:Cstd::stringを取るユーザ定義のコンストラクタを有するタイプである場合、それはなるだろう:

C obj = stringA + stringB; 

違法。

+0

あなたはどのユーザ定義変換を参照していますか? 'std :: string_helper'はその場合標準ライブラリに属する​​クラスになります。あなたは発展できますか? – qdii

+0

@qdii:それでも、ユーザー定義の変換と見なされます。標準ライブラリのクラスは、コンパイラに関する限り、通常のクラス(マジックなし)です。 –

+0

@qdii:「ユーザ定義」は「言語に組み込まれていない」ことを意味します。標準ライブラリは「ユーザ」としてカウントされます。 –

関連する問題