2012-03-08 1 views
4

Давайте рассмотрим этот фрагмент и предположим, что a, b, c и d являются непустыми строками.std :: string и несколько конкатенаций

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

При вычислении суммы этих 3 std::string случаев, стандартные реализации библиотек создают первый временный std::string объект, копировать в своем внутреннем буфере объединенных буферов a и b, а затем выполнить ту же операцию между временной строкой и c.

Один программист подчеркивал, что вместо этого можно было бы определить operator+(std::string, std::string), чтобы вернуть std::string_helper.

Настоящая роль этого объекта заключается в том, чтобы отложить фактические конкатенации до момента, когда он был заброшен в std::string. Очевидно, что operator+(std::string_helper, std::string) будет определен для возврата того же помощника, который «будет иметь в виду» тот факт, что он имеет дополнительную конкатенацию для выполнения.

Такое поведение позволит сэкономить затраты ЦП на создание временных объектов n-1, выделить их буфер, скопировать их и т. Д. Поэтому мой вопрос: почему он не работает так? Я не могу придумать любой недостаток или ограничение.

+1

Самым очевидным недостатком является сложность. – PlasmaHH

+0

В C++ 11 временное может быть повторно использовано с помощью ссылок rvalue. – avakar

+4

@PlasmaHH: Сложность скрыта от пользователя, поэтому не особенно плохо.Главный недостаток заключается в том, что он вводит неявное пользовательское преобразование типов, которое разбивает существующий код, который полагается на неявное преобразование из 'std :: string'. –

ответ

6

Почему он не работает так?

Я могу только догадываться о том, почему он был изначально разработан таким образом. Возможно, дизайнеры струнной библиотеки просто не думали об этом; возможно, они думали, что дополнительное преобразование типа (см. ниже) может привести к слишком неожиданному поведению в некоторых ситуациях. Это одна из старейших библиотек C++, и много мудрости, которую мы считаем само собой разумеющимся, просто не существовало в последние десятилетия.

Относительно того, почему он не был изменен для работы следующим образом: он может сломать существующий код, добавив дополнительное пользовательское преобразование типов. Неявные преобразования могут включать не более одного пользовательского преобразования. Это определяется C++ 11, 13.3.3.1.2/1:

Последовательность преобразования определяется пользователь состоит из начальной стандартной последовательности конверсии с последующей определенного пользователем преобразования с последующей вторым стандартом последовательность преобразования.

Рассмотрим следующий пример:

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

void f(thingy); 

f(some_string + another_string); 

Этот код прекрасно, если тип some_string + another_string является std::string. Это может быть неявно преобразовано в thingy через конструктор преобразования. Однако, если бы мы изменили определение operator+, чтобы дать другой тип, тогда потребуется два преобразования (string_helper - string - thingy), и поэтому не удалось скомпилировать.

Итак, если скорость стробоскопа важна, вам нужно использовать альтернативные методы, такие как конкатенация с +=. Или, по словам Маттиу, не беспокойтесь об этом, потому что C++ 11 исправляет неэффективность по-другому.

+2

Техника была хорошо известна, когда я учился на C++ (около 1990 года), поэтому я сомневаюсь, что причина в том, что оригинальный дизайнер не слышал об этом. Скорее всего, он считал, что это плохой дизайн для типичных применений, ожидаемых от 'std :: string'. –

+0

@JamesKanze: достаточно справедливо; мои знания вернулись только к середине девяностых, поэтому я могу только догадываться о более раннем развитии. –

+0

@Mike: но 'std :: string_helper' будет иметь неявный оператор приведения к' std :: string', этого было бы недостаточно для компиляции кода? – qdii

2

Мне кажется, что-то вроде этого уже существует: std::stringstream.

Только у вас есть << вместо +. Просто потому, что существует std::string::operator +, он не делает его наиболее эффективным вариантом.

0

Я думаю, что если вы используете +=, то он будет немного быстрее:

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

Это должно быть быстрее, так как он не создает временный objects.Or просто это,

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

теперь, когда у нас есть ссылки rvalue, это не быстрее. –

+0

@MooingDuck: Что именно не быстрее? – Nawaz

+0

Любой код в вашем сообщении должен быть одним memcpy, равным 12 байтам, чем код в OP. –

0

Основная причина, по которой не выполнить строку отдельных команд +, и особенно не делать этого в цикле, состоит в том, что есть O (n) сложность.

Разумная альтернатива с O ( п) сложность заключается в использовании простой строковый построитель, как

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 

Затем вы можете создавать эффективные и понятные вещи, как и hellip;

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+ перегружен для ссылок rvalue.Это означает, что:

d = a + b + c; 

превращается в:

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

который (почти) столь же эффективным, как вы можете получить.

Единственная неэффективность, оставшаяся в версии C++ 11, заключается в том, что память не резервируется раз и навсегда в начале, поэтому может быть перераспределение и копирование до 2 раз (для каждой новой строки). Тем не менее, поскольку добавление амортизируется O (1), если C не является более длинным, чем B, то в худшем случае должна иметь место одна копия перераспределения +. И, конечно же, мы говорим о копировании POD здесь (так называется звонок memcpy).

+0

+1: Это интересно. Что вы подразумеваете под «добавлением амортизируется O (1)»? – qdii

+0

@qdii: Амортизированный O (1) - это термин, используемый в анализе сложности алгоритма. Это означает, что это не всегда O (1) (так как иногда добавляет триггеры перераспределение + копия памяти), но * в среднем * это O (1). Это делается в основном путем экспоненциального роста базового буфера, так что перераспределение становится все менее и менее необходимым по мере роста вещей. Например, удвоение хранилища каждый раз, когда требуется больше хранения, является адекватной стратегией. –

6

Очевидный ответ: потому что стандарт не позволяет этого. Это влияет код пути введения дополнительного определенного пользователя преобразования в некоторых случаях: если C типа, имеющий определенный пользователем конструктор, принимающий в std::string, то это было бы сделать:

C obj = stringA + stringB; 

незаконным.

+0

, на которое вы указали пользовательское преобразование? 'std :: string_helper' будет классом, принадлежащим к стандартной библиотеке в этом случае. Не могли бы вы развиться? – qdii

+0

@qdii: даже так, это будет считаться определяемым пользователем преобразованием. Классы стандартной библиотеки - это обычные классы (без магии) в отношении компилятора. –

+0

@qdii: «пользовательский» означает «не встроенный в язык»; стандартная библиотека считается «пользователем». –