2

I'm using a wrapper class around std::string, but what's the easiest/cleanest/most complete way to initialize/construct it. I need at least 3 ways

  • From string literals
  • From std::string rvalues, avoiding copy!?
  • From string_view and string (yes, copy)

The naive programmer would just want to auto-delegate any construction to std::string, but that's not a feature.

struct SimpleString
{
    SimpleString() = default;
    
    template<typename T>
    SimpleString( T t ) : Text( t ) { }   // <==== experimental
    
    // Alternative: are these OK
    SimpleString( const char* text ) : Text( text ) { }
    SimpleString( std::string&& text ) : Text( text ) { }
    SimpleString( const std::string_view text ) : Text( text ) { }

    std::string Text;
};

Preemptive note: Yes, I want it and I need it. Use case: call a generic function where SimpleString is treated differently from std::string.

Note regarding inheriting from std::string: Probably a bad idea because implicit conversions will occur at the first opportunity.

non-user38741
  • 671
  • 5
  • 19
  • 3
    Why do you need a `std::string` wrapper? What's the use case? What features is it supposed to provide that `std::string` doesn't have? If you just need to add a little bit of functionality to it, you would be best-of just inheriting from it. – Jan Schultke Aug 17 '20 at 17:26
  • @J.Schultke have you read preemptive note? – SergeyA Aug 17 '20 at 17:32
  • See [Derive class off of std::string class to add extra function?](https://stackoverflow.com/questions/30029385/) – Remy Lebeau Aug 17 '20 at 18:33
  • I would instantly use that derived class variant, but thought it was frowned upon (for whatever reason). Should be OK, if my class is just a wrapper adding nothing!? What if I want add a flag? Ah, the reason to frown are implicit conversions to std::string, which I need to avoid. – non-user38741 Aug 17 '20 at 23:35
  • There is a very interesting talk by Josuttis talking about a similar case and the problems of `move` here: https://youtu.be/PNRju6_yn3o?t=233 – non-user38741 Aug 18 '20 at 00:34

2 Answers2

7

If your primary goal is for the string-conversion constructors, you don't need to go through any huge hoops. The simplest and best approach would be to just accept a std::string by-value on the API and std::move it into place.

class SimpleString
{
public:
    SimpleString() = default;
    SimpleString(std::string text) : Text(std::move(text)) { }
    // ... other constructors / assignment ...

private:
    std::string Text;
};

Doing this, we can leverage the fact that std::string is already constructible by:

  • C-strings/literals,
  • std::string_view,
  • Other std::string objects (either lvalue or rvalue), and
  • Any user-defined type that may already be convertible to std::string

Doing this also gives the user the choice to now decide whether they want to construct the SimpleString by moving their current std::string object, or by allowing SimpleString to explicitly copy it. This gives much more flexibility to the caller.

This works out nicely since you are already going to be owning this std::string anyway, and so accepting it by-value and std::moveing the object is a simple way to "seat" this value. Moving a std::string is reasonably cheap, amounting to a few assignments for the pointer, the size, and the allocator -- so this produces negligible performance overhead while also being a simple and maintainable approach.


Modern C++ and move-semantics have made it exactly this easy to accept these kinds of types. You definitely could go and overload all N ways of constructing it (const char*, std::string_view, Ts that are convertible to strings, etc); but it would be much simpler to just accept the type as-is and move it. It's the most flexible approach while also keeping it simple and maintainable.


Here are some benchmarks to compare using a template vs using std::string + std::move. In general, it's much more favorable to keep it simple. Your coworkers and future maintainers will thank you (even if that future maintainer is you, a year from now).

Human-Compiler
  • 11,022
  • 1
  • 32
  • 59
  • *Modern C++ and move-semantics* - you mean **legacy** C++ move semantics? ;) – SergeyA Aug 17 '20 at 17:35
  • It's weird to think that C++11 and move-semantics have been standardized for _nearly a decade_... And yet, my organization still uses C++11 – Human-Compiler Aug 17 '20 at 17:41
  • @J.Schultke I'd be curious to know how many people are authoring the standard library and need to abide by such archaic nonsense. Most people write code for other developers (e.g. in a workplace), and as such should focus on maintainable solutions. Even in terms of "performance" and "efficiency", you would be suffering nearly negligible overhead caused by a swap of pointers for a move overhead since this already sinks to a destination of a `std::string` construction. I advise you to learn about profiling – Human-Compiler Aug 17 '20 at 17:51
  • 1
    @J.Schultke the argument about standard library is moot. It deals with unknown types, and can't make a decision whether calling this type's move constructor is an (almost) no-op or not. Because of that, it often has to go extra length. In this case we know the type, and we know that move constructor of `std::string` is extremely cheap. Last, but not the list, in standard library you will **never** find (at least I haven't seen it) a greedy templated constructor like that, precisely for the reasons we are trying to explain. – SergeyA Aug 17 '20 at 17:56
  • 1
    I've added [some benchmarks](https://quick-bench.com/q/txrxC4vOXFvMOX2APly3TKa0-Dw). Both solutions produce nearly identical performance in practice in terms of string construction. – Human-Compiler Aug 17 '20 at 17:58
  • 1
    @J.Schultke OP did not state he needs all the underlying constructors to be callable. The by-value solution allows a user to simply call `std::string{n, c}` as an argument to `SimpleStringWrapper` and it will still have effectively identical performance characteristics -- and this is much more maintainable than a complex template – Human-Compiler Aug 17 '20 at 18:00
  • 1
    I showed active benchmarks, not destruction. That doesn't even make sense; you can't destruct an object without constructing it. The benchmark link records the times for whatever is in the loop, and is tested _at scale_. The result is not optimized out due to the `DoNotOptimize`. You're showing me assembly, not benchmarks. You should read up on Google Bench -- it's a really cool tool to learn about real benchmarking. – Human-Compiler Aug 17 '20 at 18:06
  • You can't simply count assembly and equate that to performance; real hardware doesn't work that way. At any rate, this discussion is bordering on off-topic at this point so I won't engage any further. I genuinely do recommend you look into benchmarking practices, and Google Bench for C++. It's a really neat utility for determining how things actually perform on real hardware, and it's quite impressive to see how modern C++ practices actually perform. I use it often at work in embedded environments to ensure that things are behaving well in real-time. – Human-Compiler Aug 17 '20 at 18:11
-2

std::string has a lot of constructors and it would be troublesome to have to re-implement all of them in your class. This is why you would be best-suited forwarding to the constructors of std::string like this:

#include <utility>

struct SimpleString
{
    template <typename... Args>
    SimpleString(Args &&... args) : Text(std::forward<Args>(args)...)
    {
    }

    std::string Text;
};

To avoid conflicts with the default and copy/move constructors, we will need to use SFINAE:

template <typename... Args>
constexpr bool is_empty_pack_v = sizeof...(Args) == 0;

template <typename T, typename... Args>
constexpr bool is_copy_or_move_pack_v = sizeof...(Args) == 1 &&
                                                   (... && std::is_same_v<T, std::remove_reference_t<Args>>);

struct SimpleString
{
    SimpleString() = default;
    SimpleString(const SimpleString &) = default;
    SimpleString(SimpleString &&) = default;

    template <typename... Args, std::enable_if_t<
        not is_empty_pack_v<Args...> &&
        not is_copy_or_move_pack_v<SimpleString, Args...> &&
        std::is_constructible<std::string, Args...>, int> = 0>
    SimpleString(Args &&... args) : Text(std::forward<Args>(args)...)
    {
    }

    std::string Text;
};
Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/220008/discussion-on-answer-by-j-schultke-how-to-best-define-construct-initialize-a-st). – Machavity Aug 18 '20 at 02:49