Печать списков с запятыми C++
Я знаю, как сделать это на других языках, но не на C++, который я вынужден использовать здесь.
У меня есть набор строк, которые я печатаю в списке, и между ними нужна запятая, а не запятая. В java, например, я использовал строитель строк и просто удалял запятую с конца после того, как собрал строку. Как мне сделать это в C++?
auto iter = keywords.begin();
for (iter; iter != keywords.end( ); iter++ )
{
out << *iter << ", ";
}
out << endl;
Я сначала попытался вставить этот блок, чтобы сделать это (перемещая печать запятой здесь)
if (iter++ != keywords.end())
out << ", ";
iter--;
Я ненавижу, когда мелочи сбивают меня с толку.
РЕДАКТИРОВАТЬ: Спасибо всем. Вот почему я размещаю подобные вещи здесь. Так много хороших ответов, и решаются по-разному. После семестра Java и ассемблера (разные классы) необходимость выполнения C++ проекта за 4 дня заставила меня задуматься. Я не только получил ответ, но и подумал о разных способах решения такой проблемы. Потрясающие.
36 ответов
Используйте infix_iterator:
// infix_iterator.h
//
// Lifted from Jerry Coffin's 's prefix_ostream_iterator
#if !defined(INFIX_ITERATOR_H_)
#define INFIX_ITERATOR_H_
#include <ostream>
#include <iterator>
template <class T,
class charT=char,
class traits=std::char_traits<charT> >
class infix_ostream_iterator :
public std::iterator<std::output_iterator_tag,void,void,void,void>
{
std::basic_ostream<charT,traits> *os;
charT const* delimiter;
bool first_elem;
public:
typedef charT char_type;
typedef traits traits_type;
typedef std::basic_ostream<charT,traits> ostream_type;
infix_ostream_iterator(ostream_type& s)
: os(&s),delimiter(0), first_elem(true)
{}
infix_ostream_iterator(ostream_type& s, charT const *d)
: os(&s),delimiter(d), first_elem(true)
{}
infix_ostream_iterator<T,charT,traits>& operator=(T const &item)
{
// Here's the only real change from ostream_iterator:
// Normally, the '*os << item;' would come before the 'if'.
if (!first_elem && delimiter != 0)
*os << delimiter;
*os << item;
first_elem = false;
return *this;
}
infix_ostream_iterator<T,charT,traits> &operator*() {
return *this;
}
infix_ostream_iterator<T,charT,traits> &operator++() {
return *this;
}
infix_ostream_iterator<T,charT,traits> &operator++(int) {
return *this;
}
};
#endif
Использование будет что-то вроде:
#include "infix_iterator.h"
// ...
std::copy(keywords.begin(), keywords.end(), infix_iterator(out, ","));
В экспериментальном C++17 готовом компиляторе, который скоро появится, вы можете использовать std::experimental::ostream_joiner
:
#include <algorithm>
#include <experimental/iterator>
#include <iostream>
#include <iterator>
int main()
{
int i[] = {1, 2, 3, 4, 5};
std::copy(std::begin(i),
std::end(i),
std::experimental::make_ostream_joiner(std::cout, ", "));
}
Живые примеры с использованием GCC 6.0 SVN и Clang 3.9 SVN
Поскольку все решили сделать это с циклами while, я приведу пример с циклами for.
for (iter = keywords.begin(); iter != keywords.end(); iter++) {
if (iter != keywords.begin()) cout << ", ";
cout << *iter;
}
Предполагая неопределенно нормальный поток вывода, так что запись в него пустой строки действительно ничего не делает:
const char *padding = "";
for (auto iter = keywords.begin(); iter != keywords.end(); ++iter) {
out << padding << *iter;
padding = ", "
}
Один из распространенных подходов состоит в том, чтобы печатать первый элемент перед циклом и зацикливать только оставшиеся элементы, предварительно печатая запятую перед каждым оставшимся элементом.
Кроме того, вы должны иметь возможность создавать свой собственный поток, который поддерживает текущее состояние линии (перед endl) и помещает запятые в соответствующем месте.
РЕДАКТИРОВАТЬ: Вы также можете использовать цикл среднего испытания, как предложено TED Это было бы что-то вроде:
if(!keywords.empty())
{
auto iter = keywords.begin();
while(true)
{
out << *iter;
++iter;
if(iter == keywords.end())
{
break;
}
else
{
out << ", ";
}
}
}
Сначала я упомянул метод "печать первого элемента перед циклом", потому что он делает тело цикла действительно простым, но любой из подходов работает нормально.
Есть много умных решений, и слишком много, которые искажают код без надежды на спасение, не позволяя компилятору выполнять свою работу.
Очевидное решение заключается в частном случае первой итерации:
bool first = true;
for (auto const& e: sequence) {
if (first) { first = false; } else { out << ", "; }
out << e;
}
Это очень простой шаблон, который:
- Не искажает цикл: с первого взгляда все еще очевидно, что каждый элемент будет повторяться.
- Позволяет больше, чем просто поставить разделитель или распечатать список, как
else
Блок и тело цикла могут содержать произвольные операторы.
Это может быть не самый эффективный код, но потенциальная потеря производительности одной хорошо предсказанной ветви, вероятно, будет омрачена огромным бегемотом, который std::ostream::operator<<
,
Что-то вроде этого?
while (iter != keywords.end())
{
out << *iter;
iter++;
if (iter != keywords.end()) cout << ", ";
}
В питоне мы просто пишем:
print ", ".join(keywords)
так почему не:
template<class S, class V>
std::string
join(const S& sep, const V& v)
{
std::ostringstream oss;
if (!v.empty()) {
typename V::const_iterator it = v.begin();
oss << *it++;
for (typename V::const_iterator e = v.end(); it != e; ++it)
oss << sep << *it;
}
return oss.str();
}
а затем просто используйте это как:
cout << join(", ", keywords) << endl;
В отличие от приведенного выше примера с питоном, где " "
строка и keywords
должен быть итерируемым из строк, здесь в этом примере C++ разделитель и keywords
может быть что угодно, например
cout << join('\n', keywords) << endl;
Мой типичный метод для создания разделителей (на любом языке) - использовать цикл с промежуточным тестированием. Код C++ будет:
for (;;) {
std::cout << *iter;
if (++iter == keywords.end()) break;
std::cout << ",";
}
(примечание: дополнительный if
перед циклом необходима проверка, если ключевые слова могут быть пустыми)
Большинство других показанных решений заканчивают тем, что делали полный дополнительный тест каждую итерацию цикла. Вы делаете ввод / вывод, поэтому время, затрачиваемое на это, не является большой проблемой, но оскорбляет мои чувства.
Чтобы избежать размещения if
внутри цикла я использую это:
vector<int> keywords = {1, 2, 3, 4, 5};
if (!keywords.empty())
{
copy(keywords.begin(), std::prev(keywords.end()),
std::ostream_iterator<int> (std::cout,", "));
std::cout << keywords.back();
}
Это зависит от типа вектора, int
, но вы можете удалить его с помощью некоторого помощника.
Я предлагаю вам просто переключить первый символ с помощью лямбды.
std::function<std::string()> f = [&]() {f = [](){ return ","; }; return ""; };
for (auto &k : keywords)
std::cout << f() << k;
Я думаю, что простота для меня лучше, поэтому, просмотрев все ответы, я подготовил свое решение (требуется c ++14):
#include <iostream>
#include <vector>
#include <utility> // for std::exchange c++14
int main()
{
std::vector nums{1, 2, 3, 4, 5}; // c++17
const char* delim = "";
for (const auto value : nums)
{
std::cout << std::exchange(delim, ", ") << value;
}
}
Пример вывода:
1, 2, 3, 4, 5
Другое возможное решение, которое позволяет избежать if
Char comma = '[';
for (const auto& element : elements) {
std::cout.put(comma) << element;
comma = ',';
}
std::cout.put(']');
Зависит от того, что вы делаете в своей петле.
Если значения std::string
s вы можете написать это красиво в декларативном стиле с помощью range-v3
#include <range/v3/all.hpp>
#include <vector>
#include <iostream>
#include <string>
int main()
{
using namespace ranges;
std::vector<std::string> const vv = { "a","b","c" };
auto joined = vv | view::join(',');
std::cout << to_<std::string>(joined) << std::endl;
}
Для других типов, которые должны быть преобразованы в строку, вы можете просто добавить вызов преобразования to_string
,
#include <range/v3/all.hpp>
#include <vector>
#include <iostream>
#include <string>
int main()
{
using namespace ranges;
std::vector<int> const vv = { 1,2,3 };
auto joined = vv | view::transform([](int x) {return std::to_string(x);})
| view::join(',');
std::cout << to_<std::string>(joined) << std::endl;
}
Попробуй это:
typedef std::vector<std::string> Container;
typedef Container::const_iterator CIter;
Container data;
// Now fill the container.
// Now print the container.
// The advantage of this technique is that ther is no extra test during the loop.
// There is only one additional test !test.empty() done at the beginning.
if (!data.empty())
{
std::cout << data[0];
for(CIter loop = data.begin() + 1; loop != data.end(); ++loop)
{
std::cout << "," << *loop;
}
}
Следующее должно сделать:-
const std::vector<__int64>& a_setRequestId
std::stringstream strStream;
std::copy(a_setRequestId.begin(), a_setRequestId.end() -1, std::ostream_iterator<__int64>(strStream, ", "));
strStream << a_setRequestId.back();
Есть небольшая проблема с ++
оператор вы используете.
Ты можешь попробовать:
if (++iter != keywords.end())
out << ", ";
iter--;
Сюда, ++
будет оцениваться перед сравнением итератора с keywords.end()
,
Я использую небольшой вспомогательный класс для этого:
class text_separator {
public:
text_separator(const char* sep) : sep(sep), needsep(false) {}
// returns an empty string the first time it is called
// returns the provided separator string every other time
const char* operator()() {
if (needsep)
return sep;
needsep = true;
return "";
}
void reset() { needsep = false; }
private:
const char* sep;
bool needsep;
};
Чтобы использовать это:
text_separator sep(", ");
for (int i = 0; i < 10; ++i)
cout << sep() << i;
Я думаю, что этот вариант ответа @MarkB обеспечивает оптимальный баланс читаемости, простоты и краткости:
auto iter= keywords.begin();
if (iter!=keywords.end()) {
out << *iter;
while(++iter != keywords.end())
out << "," << *iter;
}
out << endl;
Это очень легко исправить (взято из моего ответа здесь):
bool print_delim = false;
for (auto iter = keywords.begin(); iter != keywords.end( ); iter++ ) {
if(print_delim) {
out << ", ";
}
out << *iter;
print_delim = true;
}
out << endl;
Я использую эту идиому (шаблон?) Во многих языках программирования и во всех видах задач, в которых вам нужно создать вывод с разделителями из списка, например входы. Приведу аннотацию в псевдокоде:
empty output
firstIteration = true
foreach item in list
if firstIteration
add delimiter to output
add item to output
firstIteration = false
В некоторых случаях можно даже опустить
firstIteration
индикаторная переменная полностью:
empty output
foreach item in list
if not is_empty(output)
add delimiter to output
add item to output
Используя boost:
std::string add_str("");
const std::string sep(",");
for_each(v.begin(), v.end(), add_str += boost::lambda::ret<std::string>(boost::lambda::_1 + sep));
и вы получите строку, содержащую вектор, разделенный запятой.
РЕДАКТИРОВАТЬ: чтобы удалить последнюю запятую, просто введите:
add_str = add_str.substr(0, add_str.size()-1);
Может быть так..
bool bFirst = true;
for (auto curr = keywords.begin(); curr != keywords.end(); ++curr) {
std::cout << (bFirst ? "" : ", ") << *curr;
bFirst = false;
}
Вот два метода, которые вы можете использовать, и оба они по сути своей идеи. Мне нравятся эти методы, потому что они не содержат ненужных условных проверок или операций присваивания. Я назову первый метод print first.
Метод 1: метод первой печати
if (!keywords.empty()) {
out << *(keywords.begin()); // First element.
for (auto it = ++(keywords.begin()); it != keywords.end(); it++)
out << ", " << *it; // Every subsequent element.
}
Это метод, который я использовал сначала. Он работает, печатая первый элемент в вашем контейнере отдельно, а затем печатает каждый последующий элемент, которому предшествуют запятая и пробел. Он простой, лаконичный и отлично работает, если это все, что вам нужно. Если вы захотите сделать что-то еще, например, добавить "и" перед последним элементом, этот метод не поможет. Вам нужно будет проверять каждую итерацию цикла на предмет последнего элемента. Однако добавить точку или новую строку после списка было бы не так уж плохо. Вы можете просто добавить еще одну строку после цикла for, чтобы добавить в список все, что вы хотите.
Второй способ мне намного больше нравится. Я назову последний метод print, так как он делает то же самое, что и первый, но в обратном порядке.
Метод 2: последний метод печати
if (!keywords.empty()) {
auto it = keywords.begin(), last = std::prev(keywords.end());
for (; it != last; it++) // Every preceding element.
out << *it << ", ";
out << "and " << *it << ".\n"; // Last element.
}
Этот работает путем печати каждого элемента, кроме последнего, с запятой и пробелом, что позволяет вам при желании добавить "и" перед ним, точку после него и / или символ новой строки. Как видите, этот метод дает гораздо больше возможностей для обработки последнего элемента, не влияя на производительность цикла или не добавляя большого количества кода.
Если вам мешает оставить первую часть цикла for пустой, вы можете написать это так:
if (!keywords.empty()) {
auto it, last;
for (it = keywords.begin(), last = std::prev(keywords.end()); it != last; it++)
out << *it << ", ";
out << "and " << *it << ".\n";
}
Я думаю, что это должно работать
while (iter != keywords.end( ))
{
out << *iter;
iter++ ;
if (iter != keywords.end( )) out << ", ";
}
Этот перегружает оператор потока. Да, глобальные переменные - это зло.
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>
int index = 0;
template<typename T, template <typename, typename> class Cont>
std::ostream& operator<<(std::ostream& os, const Cont<T, std::allocator<T>>& vec)
{
if (index < vec.size()) {
if (index + 1 < vec.size())
return os << vec[index++] << "-" << vec;
else
return os << vec[index++] << vec;
} else return os;
}
int main()
{
std::vector<int> nums(10);
int n{0};
std::generate(nums.begin(), nums.end(), [&]{ return n++; });
std::cout << nums << std::endl;
}
Начиная с C++23, вы можете использоватьилиstd::print
(илиstd::println
) сделать это.
#include <print>
#include <vector>
int main() {
std::print("{}", std::vector{2, 3, 5, 7});
}
[2, 3, 5, 7]
Если вам нужен результат какstd::string
использоватьstd::format
. Обратите внимание, что для форматирования диапазонов требуется поддержка C++23, в частности реализация P2286R8 (см. возможности библиотеки C++23 ).
#include <format>
#include <vector>
int main() {
std::string text = std::format("{}", std::vector{2, 3, 5, 7});
}
Если вы придерживаетесь более старого стандарта , вы можете использовать библиотеку fmt для печати диапазонов .
#include <fmt/ranges.h>
#include <vector>
int main() {
// direct print
fmt::print("{}", std::vector{2, 3, 5, 7});
// create std::string object
std::string str = fmt::format("{}", std::vector{2, 3, 5, 7});
}
Вы можете использовать do
цикл, переписать условие цикла для первой итерации и использовать короткое замыкание &&
оператор и тот факт, что действительный поток true
,
auto iter = keywords.begin();
if ( ! keywords.empty() ) do {
out << * iter;
} while ( ++ iter != keywords.end() && out << ", " );
out << endl;
Мне нравится диапазон, основанный на
is_last_elem
контрольная работа. Это, имхо, очень читаемо:
for (auto& e : range)
{
if (!is_last_elem(e, range)) [[likely]]
os << e << ", ";
else
os << e;
}
os << std::endl;
Полный код:
С ++20:
#include <iostream>
#include <list>
#include <ranges>
#include <utility>
#include <type_traits>
#include <memory>
template <std::ranges::bidirectional_range R>
bool is_last_elem(const std::ranges::range_value_t<R>& elem, const R& range)
{
auto last_it = range.end();
std::advance(last_it, -1);
return std::addressof(elem) == std::addressof(*last_it);
}
template <std::ranges::bidirectional_range R, class Stream = std::ostream>
void print(const R& range, std::ostream& os = std::cout)
{
for (auto& e : range)
{
if (!is_last_elem(e, range)) [[likely]]
os << e << ", ";
else
os << e;
}
os << std::endl;
}
int main()
{
std::list<int> v{1, 2, 3, 4, 5};
print(v);
}
С ++17:
#include <iostream>
#include <list>
#include <utility>
#include <type_traits>
#include <memory>
template <class Range>
using value_type_t = std::remove_reference_t<decltype(*std::begin(std::declval<Range>()))>;
template <class Range>
bool is_last_elem(const value_type_t<Range>& elem, const Range& range)
{
auto last_it = range.end();
std::advance(last_it, -1);
return std::addressof(elem) == std::addressof(*last_it);
}
template <class Range, class Stream = std::ostream>
void print(const Range& range, std::ostream& os = std::cout)
{
for (auto& e : range)
{
if (!is_last_elem(e, range))
os << e << ", ";
else
os << e;
}
os << std::endl;
}
int main()
{
std::list<int> v{1, 2, 3, 4, 5};
print(v);
}
Я использую это:
template<class T>
void print(vector<T> v, ostream& f, const char* separator) {
std::copy(v.begin(), v.end()-1, std::ostream_iterator<T>(f, separator));
f << *(v.end()-1) << endl;}
вы называете это так:
print(v, std::cout, ",");
Самое гладкое решение на мой взгляд:
std::vector<std::string> keywords = { "1", "2", "3", "4", "5" };
std::cout << std::accumulate(std::next(keywords.begin()), keywords.end(), *keywords.begin(),
[](const std::string& a, const std::string& b){ return a + ", " + b; }) << std::endl;