I created a string splitter in C++. It splits a string in a lazy fashion and makes use of forward iterators to sequentially supply the next token from the string until it runs out of tokens.
#include <string>
#include <algorithm>
#include <regex>
#include <istream>
#include <iostream>
#include <vector>
#include <utility>
#include <iterator>
#include <initializer_list>
#include <type_traits>
#include <unordered_set>
class StringSplitter {
template <bool is_const = false>
class SplitIterator: public std::iterator<std::forward_iterator_tag,
typename std::conditional<is_const, const std::string, std::string>::type> {
// The type of the elements to be iterated over
using Type = typename std::iterator_traits<SplitIterator>::value_type;
using UnqualifiedType = typename std::remove_const<Type>::type;
friend class StringSplitter;
const StringSplitter& split;
UnqualifiedType token;
std::smatch results;
typename Type::const_iterator pos, fin;
/**
* @brief This constructor takes a StringSplitter reference to make use of
*
* @param splitter The underlying StringSplitter object which
* contains the string to split
*/
SplitIterator (const StringSplitter& splitter) :split(splitter) {
this->pos = splitter.string_to_split.begin();
this->fin = splitter.string_to_split.end();
this->prepare_next_token();
std::cout << "Iterator String split object copy ctor called\n";
}
SplitIterator (const StringSplitter&& splitter)
:split(std::move(splitter)) {
this->pos = split.string_to_split.begin();
this->fin = split.string_to_split.end();
this->prepare_next_token();
std::cout << "Iterator String split object move ctor called\n";
}
/**
* @brief Creates the next token which can be accessed by dereferencing
* the iterator
* @details If the iterator has run out of string to split, it will set
* the token to the empty string
* Otherwise, it will search for the next occurence of the delimiters supplied
* and return the next token available
*/
void prepare_next_token() noexcept {
if (this->pos == this->fin) {
this->token = "";
return;
}
if (!std::regex_search(this->pos, this->fin, this->results, this->split.pattern,
std::regex_constants::match_not_null)) {
// If the end of the string is reached without finding a delimiter
// then copy the rest of the string and set pos to fin
if (this->results.suffix().length() == 0) {
this->token = std::string(pos, fin);
this->pos = this->fin;
// otherwise, take the token before this delimiter
} else this->token = this->results.prefix();
} else {
this->token = this->results.prefix();
this->pos += this->results.position() + this->results.length();
}
}
/**
* @brief Used to determine if the iterator is still in a valid state
* @details This does not guarantee that the iterator is valid
* due to the possiblity of modifying token from outside the class
* @return A boolean indicating if the iterator contains a non-empty token
*/
bool is_valid() const {
return !this->token.empty();
}
public:
// Satisfy Default Constructible
SplitIterator(): SplitIterator(std::move(StringSplitter(""))) {}
SplitIterator (const SplitIterator& other)
:split(other.split), token(other.token),
results(other.results), pos(other.pos),
fin(other.fin) {
std::cout << "Iter copy ctor called\n";
}
SplitIterator (const SplitIterator&& other)
:split(std::move(other.split)), token(std::move(other.token)),
results(std::move(other.results)), pos(std::move(other.pos)),
fin(std::move(other.fin)) {
std::cout << "Iter move ctor called\n";
}
// Satisfy Swappable
void swap(SplitIterator& other) noexcept {
std::swap(this->split, other.split);
std::swap(this->pos, other.pos);
std::swap(this->fin, other.fin);
std::swap(this->token, other.token);
std::swap(this->results, other.results);
}
SplitIterator& operator++() { // Pre-increment
std::cout << "Pre-increment called!!\n";
this->prepare_next_token();
return *this;
}
SplitIterator operator++(int) { // Post-increment
std::cout << "Post-increment called!!\n";
SplitIterator prev_i = *this;
this->prepare_next_token();
return prev_i;
}
// Satisfy EqualityComparable
bool operator == (const SplitIterator& rhs) const {
return this->token == rhs.token;
}
// Satisfy EqualityComparable
bool operator != (const SplitIterator& rhs) const {
return !(*this == rhs);
}
Type& operator*() {
return token;
}
Type operator* () const {
return token;
}
Type const* operator-> () const {
return &token[0];
}
Type *operator->() {
return &token[0];
}
};
const std::string string_to_split;
std::regex pattern;
StringSplitter(const std::string& str)
:string_to_split(str) {
this->pattern = std::regex("\\s+");
std::cout << "StringSplitter Copy String ctor called\n";
}
StringSplitter(const std::string&& str)
:string_to_split(std::move(str)) {
this->pattern = std::regex("\\s+");
std::cout << "StringSplitter Move String ctor called\n";
}
public:
StringSplitter(const StringSplitter& other)
:string_to_split(other.string_to_split),
pattern(other.pattern) {
std::cout << "StringSplitter Copy ctor called\n";
}
StringSplitter(const StringSplitter &&other)
:string_to_split(std::move(other.string_to_split)),
pattern(std::move(other.pattern)) {
std::cout << "StringSplitter Move ctor called\n";
}
using iterator = SplitIterator<false>;
using const_iterator = SplitIterator<true>;
/**
* @brief Reads the string from a stream
* @details The string in the stream is read and stored in the
* StringSplitter object
*
* @param stream A valid stream that is open for reading
* @return A StringSplitter object
*/
static StringSplitter with(std::istream& stream);
/**
* @brief The string to split is supplied as argument
* @details Does not modify the original string therefore it is
* passed as a constant reference
*
* @param str The string to split
* @return A StringSplitter object
*/
static StringSplitter with(const std::string& str);
/**
* @brief Accepts an rvalue string
*
* @param str A string to split on
* @return A StringSplitter object
*/
static StringSplitter with(const std::string&& str);
/**
* @brief Split the string by a regex pattern
* @details The pattern supplied must be a valid regex but shouldn't
* have any quantifiers associated with it
* For example, to split on white space, the string should be
* "\\s" not "\\s*" or "\\s+"
*
* @param reg The string containing the pattern to split on
* @return A String splitter
*/
const StringSplitter by_pattern(const std::string&);
/**
* @brief Allows the splitting of the string through the use of characters
* @details If you want to split instead by some characters, then you can
* specify those characters as the parameters to this method
*
* @param ch An array of characters
* @return A String splitter
*/
const StringSplitter by_any(const std::initializer_list<char> chars);
/**
* @brief Can be used to create a vector from the split string
* @return A vector containing the split string
*/
operator std::vector<std::string> () const;
// Iterator stuff
iterator begin();
iterator begin() const;
const_iterator cbegin();
iterator end();
iterator end() const;
const_iterator cend();
};
StringSplitter StringSplitter::with(std::istream& iss) {
std::ostream* ptr = iss.tie(nullptr);
// http://insanecoding.blogspot.ca/2011/11/how-to-read-in-file-in-c.html
std::string content;
iss.seekg(0, std::ios::end);
content.resize(iss.tellg());
iss.read(&content[0], content.size());
iss.tie(ptr);
return StringSplitter::with(content);
}
StringSplitter StringSplitter::with(const std::string& str) {
return StringSplitter(str);
}
StringSplitter StringSplitter::with(const std::string&& str) {
return StringSplitter(std::move(str));
}
const StringSplitter StringSplitter::by_any(const std::initializer_list<char> chars) {
std::string reg = std::accumulate(chars.begin(), chars.end(), std::string{},
[](std::string& str, const char c) {
return str.empty() ? str + "[" + c : str + "|" + c;
}) + "]";
return StringSplitter::by_pattern(reg);
}
const StringSplitter StringSplitter::by_pattern(const std::string& str) {
this->pattern = str + '*';
return *this;
}
StringSplitter::operator std::vector<std::string> () const {
return std::vector<std::string>{begin(), end()};
}
StringSplitter::iterator StringSplitter::begin() {
std::cout << "Begin called!\n";
return iterator(*this);
}
StringSplitter::iterator StringSplitter::begin() const {
std::cout << "CV Begin called!\n";
return iterator(*this);
}
StringSplitter::const_iterator StringSplitter::cbegin() {
std::cout << "Const Begin called!\n";
return const_iterator(*this);
}
StringSplitter::iterator StringSplitter::end() {
std::cout << "End called!\n";
return iterator();
}
StringSplitter::iterator StringSplitter::end() const {
std::cout << "CV End called!\n";
return iterator();
}
StringSplitter::const_iterator StringSplitter::cend() {
std::cout << "Const End called!\n";
return const_iterator();
}
I have some concerns with the code. Here is the main method that makes use of the class:
auto main() -> int {
std::vector<std::string> tokens =
StringSplitter::with("Hello my !; name is!Slim Shady!")
.by_any({' ', '!', ';'});
for (const auto& str : tokens) {
std::cout << str << std::endl;
}
return 0;
}
The output is this:
StringSplitter Move String ctor called StringSplitter Copy ctor called CV Begin called! Iterator String split object copy ctor called CV End called! StringSplitter Move String ctor called Iterator String split object move ctor called Iter copy ctor called Iter copy ctor called Iter copy ctor called Iter copy ctor called Iter copy ctor called Iter copy ctor called Iter copy ctor called Iter copy ctor called Pre-increment called!! Pre-increment called!! Pre-increment called!! Pre-increment called!! Pre-increment called!! Pre-increment called!! Iter copy ctor called Iter copy ctor called Iter copy ctor called Iter copy ctor called Iter copy ctor called Iter copy ctor called Pre-increment called!! Pre-increment called!! Pre-increment called!! Pre-increment called!! Pre-increment called!! Pre-increment called!! Hello my name is Slim Shady
This output is my main concern with the code. I admit I went a bit wild the constructors, but it's been a while since I have played with OOP in C++, so the idea of rvalues, lvalues, gvalues, move ctors, copy ctors, etc was very confusing.
- Why are there soo many calls to copying the iterator. The
++()
operator for the iterator class returns a reference, so I don't understand why there is soo much copying going on - Also why does it seem to be doing everything twice? As you can see from the main method, it is only used once but the output looks like it was used twice.
- Suggestions are welcome even if it means scrapping the current implementation.
const T&&
is nonsense. You cannot move from a const object or reference, so in practice that is the same as a const ref. You should drop the const in those overloads. \$\endgroup\$