2012-03-08 4 views
4

Considérons cet extrait, et supposons que a, b, c et d sont des chaînes non vides.std :: string et plusieurs concaténations

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

Lors du calcul de la somme de ces 3 cas std::string, les mises en œuvre de la bibliothèque standard de créer un premier objet std::string temporaire, copie dans sa mémoire tampon interne tampons concaténées de a et b, puis effectuer les mêmes opérations entre la chaîne temporaire et le c. Un codeur programmeur soulignait qu'au lieu de ce comportement, operator+(std::string, std::string) pouvait être défini pour renvoyer un .

Le rôle même de cet objet serait de différer les concaténations réelles au moment où il est converti en std::string. Évidemment, operator+(std::string_helper, std::string) serait défini pour retourner le même assistant, ce qui "garderait à l'esprit" le fait qu'il a une concaténation supplémentaire à effectuer. Un tel comportement permettrait d'économiser le coût CPU de la création de n-1 objets temporaires, l'allocation de leur tampon, leur copie, etc Donc, ma question est: pourquoi ne fonctionne-t-il pas déjà comme ça? Je ne peux pas penser à tout inconvénient ou limitation.

+1

L'inconvénient le plus évident est la complexité ajoutée. – PlasmaHH

+0

En C++ 11, le temporaire peut être réutilisé grâce aux références rvalue. – avakar

+4

@PlasmaHH: La complexité est cachée à l'utilisateur, donc pas particulièrement mauvaise.L'inconvénient principal est qu'il introduit une conversion de type implicite définie par l'utilisateur, qui casserait le code existant qui repose sur une conversion implicite de 'std :: string'. –

Répondre

6

Pourquoi ça ne marche pas déjà comme ça?

Je ne peux que spéculer sur pourquoi il a été conçu à l'origine comme ça. Peut-être que les concepteurs de la bibliothèque à cordes n'y ont tout simplement pas pensé; peut-être qu'ils pensaient que la conversion de type supplémentaire (voir ci-dessous) pourrait rendre le comportement trop surprenant dans certaines situations. C'est l'une des plus anciennes bibliothèques C++, et beaucoup de sagesse que nous prenons pour acquis n'existait tout simplement pas au cours des dernières décennies.

En ce qui concerne les raisons pour lesquelles cela n'a pas été modifié, cela pourrait casser le code existant, en ajoutant une conversion de type supplémentaire définie par l'utilisateur. Les conversions implicites peuvent uniquement impliquer au plus une conversion définie par l'utilisateur. Ceci est spécifié par C++ 11, 13.3.3.1.2/1:

Une séquence de conversion défini par l'utilisateur consiste en une séquence initiale de conversion standard suivie d'une conversion définie par l'utilisateur suivie d'une seconde norme séquence de conversion.

Considérez ce qui suit:

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

void f(thingy); 

f(some_string + another_string); 

Ce code est très bien si le type de some_string + another_string est std::string. Cela peut être implicitement converti en thingy via le constructeur de conversion. Cependant, si nous devions changer la définition de operator+ pour donner un autre type, alors il faudrait deux conversions (string_helper à string à thingy), et ainsi échouerait à compiler. Par conséquent, si la vitesse de construction des chaînes est importante, vous devrez utiliser d'autres méthodes telles que la concaténation avec +=. Ou, selon la réponse de Matthieu, ne vous inquiétez pas parce que C++ 11 corrige l'inefficacité d'une manière différente.

+2

La technique était bien connue quand j'apprenais le C++ (vers 1990), donc je doute que la raison en soit que le concepteur d'origine n'en avait pas entendu parler. Plus probablement, il a estimé que c'était un mauvais design pour les utilisations typiques attendues de 'std :: string'. –

+0

@JamesKanze: assez bien; mes connaissances remontent au milieu des années 1990, et je ne peux que spéculer sur des développements antérieurs. –

+0

@Mike: mais 'std :: string_helper' aurait un opérateur de cast implicite à' std :: string', cela ne serait-il pas suffisant pour compiler le code? – qdii

2

Cela me semble que quelque chose comme ça existe déjà: std::stringstream.

Seulement vous avez << au lieu de +. Tout simplement parce qu'il existe std::string::operator +, il ne constitue pas l'option la plus efficace.

0

Je pense que si vous utilisez +=, alors il sera peu plus vite:

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

Il devrait être plus rapide, car il ne crée pas objects.Or temporaire simplement cela,

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

maintenant que nous avons des références rvalue, ce n'est pas plus rapide. –

+0

@MooingDuck: Qu'est-ce qui ne va pas plus vite? – Nawaz

+0

Tout le code dans votre message doit être un memcpy de 12 octets de moins que le code dans l'OP. –

0

La raison principale pour ne pas faire une chaîne de concaténations individuelles +, et en particulier ne pas le faire dans une boucle, est qu'il a O ( n) complexité.

Une alternative raisonnable avec O (n ) complexité est d'utiliser un simple constructeur de chaîne, comme

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 

Ensuite, vous pouvez écrire des choses efficaces et claires comme & hellip;

using narrow::S; 

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

Cela dépend.

En C++ 03, il est exact qu'il peut y avoir une légère inefficacité là-bas (comparable à Java et C# car ils utilisent d'ailleurs le string interning). Cela peut être atténué en utilisant:

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

ce qui n'est pas vraiment ... idiomatique.

En C++ 11, operator+ est surchargé pour les références rvalue.Ce qui signifie que:

d = a + b + c; 

est transformé en:

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

qui est (presque) aussi efficace que vous pouvez obtenir. La seule inefficacité restante dans la version C++ 11 est que la mémoire n'est pas réservée une fois pour toutes au début, donc il pourrait y avoir une réaffectation et des copies jusqu'à 2 fois (pour chaque nouvelle chaîne). Cependant, comme l'addition est amortie O (1), à moins que C soit plus long que B, alors au pire une seule réallocation + copie devrait avoir lieu. Et bien sûr, nous parlons de copie POD ici (donc un appel memcpy).

+0

+1: C'est intéressant. Que voulez-vous dire par "l'ajout est amorti O (1)"? – qdii

+0

@qdii: Amortized O (1) est un terme utilisé dans l'analyse de complexité de l'algorithme. Cela signifie que ce n'est pas toujours O (1) (puisque parfois l'ajout déclenche une réallocation + copie de la mémoire), mais * en moyenne * c'est O (1). Cela se fait généralement en ayant une croissance exponentielle du tampon sous-jacent, de sorte que les réaffectations sont de moins en moins souvent nécessaires au fur et à mesure que les choses grandissent. Par exemple, doubler le stockage chaque fois que plus de stockage est requis est une stratégie adéquate. –

6

La réponse évidente: parce que la norme ne le permet pas. Code des impacts en introduisant un utilisateur supplémentaire conversion définie dans certains cas: si C est un type ayant un constructeur défini par l'utilisateur en prenant un std::string, alors il serait:

C obj = stringA + stringB; 

illégale.

+0

quelle conversion définie par l'utilisateur faites-vous référence? 'std :: string_helper' serait une classe appartenant à la bibliothèque standard dans ce cas. Pourriez-vous développer? – qdii

+0

@qdii: même ainsi, cela serait considéré comme une conversion définie par l'utilisateur. Les classes de la bibliothèque standard sont des classes régulières (pas de magie) en ce qui concerne le compilateur. –

+0

@qdii: "défini par l'utilisateur" signifie "non intégré à la langue"; la bibliothèque standard compte comme un "utilisateur". –

Questions connexes