11
\$\begingroup\$

I often need to write string processing functions that take an input string and transform that into some output string. I'm looking for a pattern to make such functions as generic as possible (with reasonable effort).

Requirements

  • It should be possible to supply the input as a range (like std::string or std::string_view), pair of iterators, null-terminated character arrays and pointers.
  • Character type should not be restricted to char, but also support wchar_t. Support char16_t and char32_t only if it doesn't require additional effort as I currently have no use for them.
  • Easy to use for the simple case where input and output is a std::basic_string.
  • Easy to write and more importantly easy to read.
  • Performance is less important. Processing character-by-character is good enough.

I currently approach this by splitting the interface into two template functions. The first one is the idiomatic generic interface which is iterator based, the second one is range based for convenience. The convenience interface is allowed to trade some generalism for ease-of-use.

Example

Escape all occurences of the characters '\n', '\t' and '\\' in the input string.

First the idiomatic interface.

template< class InputIt, class OutputIt >
OutputIt EscapeString( InputIt first, InputIt last, OutputIt out )
{
    while( first != last )
    {
        auto c = *first++;
        switch( c )
        {
            case '\n':
                *out++ = '\\';
                *out++ = 'n';
                break;
            case '\t':
                *out++ = '\\';
                *out++ = 't';
                break;
            case '\\':
                *out++ = '\\';
                *out++ = '\\';
                break;
            default:
                *out++ = c;
        }
    }
    return out;
}

Now the convenience wrapper. InputString can be any string range (like std::string), null-terminated char array or pointer to null-terminated string. The result will be std::basic_string<character_type_of_InputString> by default, but can be changed to any class that has a push_back() member.

template< typename InputString, 
          class ResultString = typename as_literal_traits<InputString>::std_string_type >
ResultString EscapeStringCopy( const InputString& input )
{
    // Allow both the ADL-selected overloads for user-defined types and the standard 
    // library function templates to appear in the same overload set.
    using std::begin; using std::end;

    // Handle null-terminated character arrays, character pointers and ranges uniformly. 
    auto inputRange = boost::as_literal( input );

    ResultString result;
    EscapeString( begin( inputRange ), end( inputRange ), back_inserter( result ) );
    return result;
}

The template as_literal_traits is an utility that is used to create a default return type of std::basic_string that has the same character type as the input string.

template< typename StringT >
struct as_literal_traits
{
    // NOTE: std::declval<StringT const&>() is used instead of just StringT{} 
    // in case StringT is not default-constructible.
    using range_type = decltype( boost::as_literal( std::declval<StringT const&>() ) );
    using value_type = typename boost::range_value< range_type >::type;
    using std_string_type = std::basic_string< value_type >;
};

Live demo at Coliru

Any improvements are welcome.

\$\endgroup\$
  • 1
    \$\begingroup\$ Hello, do you have a minute to talk about regex. Jokes aside could you elaborate on what kind of other transform operations you perform on strings? \$\endgroup\$ – yuri Jun 8 '17 at 21:23
  • \$\begingroup\$ @yuri I don't want to discuss why I didn't choose regex (or any other string library of your choice) for the task. Please focus on the code shown. I believe most of my transforms I have written so far are similar to the "escape" algorithm shown above. \$\endgroup\$ – zett42 Jun 8 '17 at 22:05
  • 2
    \$\begingroup\$ Only thing that pops out to me is in as_literal_traits: you should use std::declval<StringT const&>() instead of StringT{} because some client StringT might not be default constructible. That would cause a compilation error. \$\endgroup\$ – user2296177 Jun 10 '17 at 22:50
  • \$\begingroup\$ @user2296177, could you make that an answer? A single useful observation makes a worthwhile answer, and I wouldn't want that to be lost in a comment. Thanks! \$\endgroup\$ – Toby Speight Apr 3 '18 at 10:34
  • \$\begingroup\$ @TobySpeight I have incorporated the suggestion in the post. \$\endgroup\$ – zett42 Apr 5 '18 at 16:28
4
\$\begingroup\$

Look at Boost.Range. Your first point drives most of the rest of the concerns.

Write your template function to take a Range. You can pass it a C string literal, a std::string, an array, vector, or anything —— even a pair of iterators if that’s what you need after all.

Not limited to char? No problem: make it a Range of T, with a default of char.

This takes the place of your initial idiomatic approach. Use ranges rather than iterators, and you don’t need to wrap it for convenience. Boost.Range does all that for you.

The output can remain an output iterator, but I like to make it a range adaptor.

With that, you can do things like:

for (auto c : mystring|escaped) { ⋯
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.