Оператор перегрузки ‹‹ — C++

Фон

У меня есть класс-контейнер, который использует внутри себя vector‹std::string›. Я предоставил метод AddChar(std::string) для этого класса-оболочки, который выполняет push_back() для внутреннего вектора. В моем коде мне нужно время от времени добавлять несколько элементов в контейнер. Для этого я должен использовать

container.AddChar("First");
container.AddChar("Second");

Это делает код больше. Поэтому, чтобы упростить задачу, я планирую перегрузить оператор ‹‹. Чтобы я мог написать

container << "First" << "Second"

и два элемента будут добавлены к базовому вектору.

Вот код, который я использовал для этого

class ExtendedVector
{
private:
    vector<string> container;

public:
    friend ExtendedVector& operator<<(ExtendedVector& cont,const std::string str){
        cont.AddChar(str);
        return cont;
    }

    void AddChar(const std::string str)
    {
        container.push_back(str);
    }

    string ToString()
    {
        string output;
        vector<string>::iterator it = container.begin();
        while(it != container.end())
        {
            output += *it;
            ++it;
        }
        return output;
    }
};

Он работает так, как ожидалось.

Вопросы

  1. Правильно ли написана перегрузка оператора?
  2. Является ли хорошей практикой перегрузка операторов в подобных ситуациях?
  3. Будут ли проблемы с производительностью или другие проблемы с этим кодом?

Какие-нибудь мысли?

Изменить

Услышав отличные комментарии, я решил не перегружать ‹‹, так как здесь это не имеет смысла. Я удалил код перегрузки оператора, и вот окончательный код.

class ExtendedVector
{
private:
    vector<string> container;

public:

    ExtendedVector& AddChar(const std::string str)
    {
        container.push_back(str);
        return *this;
    }

         .. other methods
}

Это позволяет мне добавить

container.AddChar("First").AddChar("Second")

В C# я могу сделать это проще, используя ключевое слово params. Код будет таким

void AddChar(params string[] str)
{
    foreach(string s in str)
       // add to the underlying collection
}

Я знаю, что в C++ мы можем использовать ... для указания переменной длины параметров. Но AFAIK, это не безопасно для типов. Так рекомендуется ли это делать? Чтобы я мог написать

container.AddChar("First","Second")

Спасибо за ответы.


person Navaneeth K N    schedule 02.03.2009    source источник
comment
Qt использует оператор‹‹ для QStringList: doc.trolltech.com/4.4/qstringlist.html , и мне понравилось его использование с этим. но в целом я бы смотрел, чтобы не добавлять слишком много операторов. как говорит Кевин, это может чертовски запутать :)   -  person Johannes Schaub - litb    schedule 02.03.2009


Ответы (7)


Правильно ли написана перегрузка оператора?

Это так, но можно сделать лучше. Как уже упоминалось, ваша функция может быть полностью определена из существующих общедоступных функций. Почему бы не использовать только их? Прямо сейчас это друг, а значит, относится к деталям реализации. То же самое верно, если вы поместите оператор ‹‹ в качестве члена в свой класс. Однако сделайте своего оператора‹‹ функцией не членом, не другом.

class ExtendedVector {
    ...
};

// note, now it is *entirely decoupled* from any private members!
ExtendedVector& operator<<(ExtendedVector& cont, const std::string& str){
    cont.AddChar(str);
    return cont;
}

Если вы измените свой класс, вы не будете уверены, что ваш оператор‹‹ все еще будет работать. Но если ваш оператор‹‹ полностью зависит только от публичных функций, то вы можете быть уверены, что он будет работать после внесения изменений только в детали реализации вашего класса. Ура!

Является ли хорошей практикой перегрузка операторов в подобных ситуациях?

Как сказал еще один парень, это спорно. Во многих ситуациях перегрузка операторов будет выглядеть «аккуратно» на первый взгляд, но в следующем году будет казаться адом, потому что вы больше не представляете, что вы имели в виду, когда относились к некоторым символам с особой любовью. В случае оператора ‹‹, я думаю, это нормальное использование. Его использование в качестве оператора вставки для потоков хорошо известно. И я знаю приложения Qt и KDE, которые широко используют его в таких случаях, как

QStringList items; 
items << "item1" << "item2";

Аналогичный случай — boost.format, который также повторно использует operator% для передачи аргументов для заполнителей в своей строке:

format("hello %1%, i'm %2% y'old") % "benny" % 21

Конечно, также спорно использовать его там. Но его использование для указаний формата printf хорошо известно, и поэтому его использование там тоже в порядке, имхо. Но, как всегда, стиль также субъективен, так что относитесь к нему с недоверием :)

Как я могу принимать аргументы переменной длины безопасным способом?

Что ж, есть способ принять вектор, если вы ищете однородные аргументы:

void AddChars(std::vector<std::string> const& v) {
    std::vector<std::string>::const_iterator cit =
        v.begin();
    for(;cit != v.begin(); ++cit) {
        AddChar(*cit);
    }
}

Хотя проезжать его не совсем удобно. Вы должны построить свой вектор вручную, а затем пройти... Я вижу, у вас уже есть правильное представление о функциях стиля vararg. Не следует использовать их для такого рода кода и только при взаимодействии с кодом C или функциями отладки, если вообще. Другой способ справиться с этим случаем — применить программирование препроцессора. Это сложная тема и довольно хакерская. Идея состоит в том, чтобы автоматически генерировать перегрузки до некоторого верхнего предела примерно так:

#define GEN_OVERLOAD(X) \
void AddChars(GEN_ARGS(X, std::string arg)) { \
    /* now access arg0 ... arg(X-1) */ \
    /* AddChar(arg0); ... AddChar(arg(N-1)); */ \
    GEN_PRINT_ARG1(X, AddChar, arg) \
}

/* call macro with 0, 1, ..., 9 as argument
GEN_PRINT(10, GEN_OVERLOAD)

Это псевдокод. Вы можете ознакомиться с библиотекой препроцессора boost здесь.

Следующая версия C++ предложит гораздо лучшие возможности. Списки инициализаторов можно использовать:

void AddChars(initializer_list<std::string> ilist) {
    // range based for loop
    for(std::string const& s : ilist) {
        AddChar(s);
    }
}

...
AddChars({"hello", "you", "this is fun"});

В следующем C++ также возможно поддерживать произвольное количество аргументов (смешанного типа), используя вариативные шаблоны. GCC4.4 будет поддерживать их. GCC 4.3 уже частично их поддерживает.

person Johannes Schaub - litb    schedule 02.03.2009
comment
В основном вы видите container << elt1 << elt2, но затем вы можете использовать Eigen, у которого есть container << elt1, elt2. Я предполагаю, что он сохраняет один непробельный символ для каждого элемента, но является ярким примером людей, не придерживающихся статус-кво :) - person Kuba hasn't forgotten Monica; 19.06.2012

1) Да, за исключением того, что, поскольку AddChar является общедоступным, нет причин, по которым он должен быть friend.

2) Это спорно. << находится в положении оператора, чья перегрузка для "странных" вещей, по крайней мере, неохотно принимается.

3) Ничего очевидного. Как всегда, профилирование — ваш друг. Возможно, вы захотите рассмотреть возможность передачи строковых параметров в AddChar и operator<< по константной ссылке (const std::string&), чтобы избежать ненужного копирования.

person Logan Capaldo    schedule 02.03.2009
comment
+1 для ссылки на std::string, чтобы избежать копирования. И для всего остального. - person Frank; 02.03.2009
comment
действительно, я тоже дал вам +1. особенно мне нравится #1. Я проясню: сделайте это функцией, не являющейся членом, не являющейся другом :), и мне тоже нравится # 2. вместо того, чтобы говорить плохое плохое, вы даете основу для аргументов :) и я думаю, что op‹‹ хорошо воспринимается как операция вставки. - person Johannes Schaub - litb; 02.03.2009

Является ли хорошей практикой перегрузка операторов в подобных ситуациях?

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

person Kevin    schedule 02.03.2009
comment
Я согласен. Перегрузка операторов может быть крутой, но когда вы пытаетесь поддерживать код несколько месяцев спустя, даже если вы его написали, вы хотите иметь возможность понять, что он делает с минимальными усилиями. Четкий код лучше крутого кода, который сбивает с толку. - person Rob; 02.03.2009
comment
Я согласен. Пока вы не измените ЗНАЧЕНИЕ оператора. т.е. MyWebRequest = URL-адрес; не должен ДЕЙСТВИТЕЛЬНО выполнять веб-запрос. Для таких вещей я предпочитаю либо цепочку функций, либо функцию, принимающую массив. - person Rahly; 24.08.2016

Лично я бы предпочел не перегружать его таким образом, потому что векторы обычно не имеют перегруженного оператора сдвига влево - на самом деле это не идиома ;-)

Вместо этого я бы, вероятно, вернул ссылку из AddChar так:

ExtendedVector& AddChar(const std::string& str) {
    container.push_back(str);
    return *this;
}

так что вы можете сделать

container.AddChar("First").AddChar("Second");

что на самом деле не намного больше, чем операторы битового сдвига.

(также см. комментарий Логана о передаче строк по ссылке, а не по значению).

person Peter    schedule 02.03.2009
comment
Ты опередил меня в этом! Через, ммм, несколько минут... ;) - person Frank; 02.03.2009
comment
Но это не вектор. Это класс, который просто хранит вектор внутри. operator‹‹ может иметь смысл. В конце концов, это уже стандарт для потоковых классов. Хотя нужно немного больше контекста, чтобы сказать наверняка. :) - person jalf; 02.03.2009
comment
Да, согласен, технически это не вектор. Но мое впечатление (которое может быть неверным, поскольку у нас нет всего этого) состоит в том, что он добавляет к нему только несколько функций, поэтому он очень похож на один. В любом случае мое предложение носит стилистический характер, поэтому у других пробег может отличаться :-) - person Peter; 02.03.2009

Перегрузка оператора в этом случае не является хорошей практикой, так как делает код менее читаемым. Стандартный std::vector также не имеет его для выталкивания элементов по уважительным причинам.

Если вас беспокоит слишком длинный код вызывающей стороны, вы можете рассмотреть это вместо перегруженного оператора:

container.AddChar("First").AddChar("Second");

Это будет возможно, если у вас есть AddChar() возврат *this.

Забавно, что у тебя есть эта функция toString(). В этом случае operator<< для вывода в поток будет стандартной вещью, которую можно использовать вместо этого! Поэтому, если вы хотите использовать операторы, сделайте функцию toString() функцией operator<<.

person Frank    schedule 02.03.2009
comment
Это, по крайней мере, так же запутанно и неожиданно, как семантика ‹‹ - person Assaf Lavie; 02.03.2009

Здесь оператор неправильно перегружен. Нет причин делать оператор другом, поскольку он может быть членом класса. Friend предназначен для функций, которые не являются фактическими членами класса (например, при перегрузке ‹‹ для ostream, чтобы объект можно было вывести в cout или ofstreams).

Что вы на самом деле хотите, чтобы оператор был:

ExtendedVector& operator<<(const std::string str){
    AddChar(str);
    return *this;
}

Обычно считается плохой практикой перегружать операторов таким образом, чтобы они делали что-то большее, чем обычно. ‹‹ обычно является битовым сдвигом, поэтому такая перегрузка может сбивать с толку. Очевидно, что STL перегружает ‹‹ для "потоковой вставки", и поэтому может иметь смысл перегрузить его для вашего использования аналогичным образом. Но это не похоже на то, что вы делаете, поэтому вы, вероятно, захотите этого избежать.

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

person SoapBox    schedule 02.03.2009

Это сделает вещи довольно запутанными, я бы использовал тот же синтаксис, что и std::cin в переменной:

std::cin >> someint;

"First" >> container;

Таким образом, это как минимум оператор вставки. Для меня, когда что-то имеет перегруженный оператор ‹‹, я ожидаю, что он что-то выведет. Так же, как std::cout.

person X-Istence    schedule 02.03.2009
comment
Хм. Я думаю о ›› как о извлечении из входного потока в мои объекты! - person Simon Buchan; 02.03.2009
comment
Я не думаю, что есть правильный ответ, я не верю, что оператор ‹‹ или ›› подходит для того, что он пытается выполнить. Лично я бы просто сохранил механизм push_back, который уже установлен, как правильный способ делать что-то, лишняя печать — это неплохо! - person X-Istence; 02.03.2009