5

NOTE: This is not about using a string for choosing the execution path in a switch-case block.

A common pattern in C++ is to use a switch-case block for converting integer constants to strings. This looks like:

char const * to_string(codes code)
{
    switch (code)
    {
        case codes::foo: return "foo";
        case codes::bar: return "bar";
    }
}

However, we are in C++, so using std::string is more appropriate:

std::string to_string(codes code)
{
    switch (code)
    {
        case codes::foo: return "foo";
        case codes::bar: return "bar";
    }
}

This however copies the string literal. Perhaps a better approach would be instead:

std::string const & to_string(codes code)
{
    switch (code)
    {
        case codes::foo: { static std::string str = "foo"; return str; }
        case codes::bar: { static std::string str = "bar"; return str; }
    }
}

But this is kinda ugly, and involves more boilerplate.

What is considered the cleanest and most efficient solution for this problem using C++14?

dv_
  • 1,247
  • 10
  • 13
  • 3
    A private hashmap with codes as key and string as value? A function to return the value based on the key. – Samer Tufail Feb 04 '19 at 14:25
  • 9
    "we are in C++, so using `std::string` is more appropriate" Says who? `std::string` is an owning mutable container, and you're returning an immutable, static value. What's wrong with `char const*`? In C++17, you have `std::string_view`, which you can implement for yourself without too much effort – KABoissonneault Feb 04 '19 at 14:25
  • @SamerTufail, thank you for validating me. I do this at work all the time. :) But was always curious if that is a good way to do things. – Duck Dodgers Feb 04 '19 at 14:26
  • `std::string` adds semantics that are quite useful, like being able to query the string length without iterating over the character array. As for implementing `string_view`, I agree this would be useful, but stuff like this is generally frowned upon in this project (I am not the lead developer there). – dv_ Feb 04 '19 at 14:27
  • @Joey Mallone why wouldnt it be? What makes you think its not the correct way to do things? – Samer Tufail Feb 04 '19 at 14:31
  • @SamerTufail, self-doubt. – Duck Dodgers Feb 04 '19 at 14:37
  • 1
    @JoeyMallone https://stackoverflow.com/questions/931890/what-is-more-efficient-a-switch-case-or-an-stdmap Allow me to "unvalidate" you for some cases :) – UKMonkey Feb 04 '19 at 14:39
  • @UKMonkey, good to know. :) – Duck Dodgers Feb 04 '19 at 14:47
  • Don't overthink this. You'll be hard pressed to find a more efficient method than to just return the string literal into the string. As a rule, don't return a string_view. It's like returning a reference. In this case it will work, but it forces a code reviewer to check your code to check. Write for simplicity and clarity. If you want to speed things up, measure. – Michael Surette Feb 04 '19 at 14:56
  • 2
    Do note that with the last code block, static initialization comes with a performance penalty because it needs to be thread safe and checked every time the function is called. That can out weigh the cost of a copy. – NathanOliver Feb 04 '19 at 15:47
  • Relevant: https://codereview.stackexchange.com/questions/14309/conversion-between-enum-and-string-in-c-class-header/ – M.M Feb 07 '19 at 23:39

3 Answers3

8

This however copies the string literal.

Yes and no. It will copy the string literal indeed, but don't necessarily allocate memory. Check your implementation SSO limit.


You could use std::string_view:

constexpr std::string_view to_string(codes code) {
    switch (code) {
        case codes::foo: return "foo";
        case codes::bar: return "bar";
    }
}

You can find many backported versions like this one

However, sometimes a char const* is the right abstraction. For example, if you were to forward that string into an API that require a null terminated string, you'd be better off returning it a c style string.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • 1
    This. A `string_view` has value for sure, but otherwise I'd just stick with the original code. Certainly there's no reason to use a `string` just because we're writing C++. C++ still has `const char*`. – Lightness Races in Orbit Feb 04 '19 at 15:12
2

But this is kinda ugly, and involves more boilerplate.

What is considered the cleanest and most efficient solution for this problem using C++14?

To answer the above, as @SamerTufail pointed out (and as I do it myself at work also), I would use enums and std::map like this.

   typedef enum {
        foo = 1,
        bar = 2,
    } Key;

std::map<Key, std::string> hash_map = { {Key::foo ,"foo"}, { Key::bar,"bar"} };

And then in main() you could get the value like this,

std::cout << hash_map.find(Key::foo)->second;

I would create a function for returning the second, where you would check the iterator for end(), otherwise the interator would be invalid and using it would be UB.


EDIT: As others have pointed out in the comments and as per this question, you could replace std::map, with std::unordered_map provided you do not need to keep elements in order.

And as per my experience, I always create such maps as static const. Therefore create them one time and use them many times to amortize the cost of creation.

Community
  • 1
  • 1
Duck Dodgers
  • 3,409
  • 8
  • 29
  • 43
  • 2
    @GuillaumeRacicot, I agree but if the map is static and created only once, the cost of using it is amortized over the lifetime of the program. No? – Duck Dodgers Feb 04 '19 at 14:44
  • Yes, true. It would avoid copying and linear seach. – Guillaume Racicot Feb 04 '19 at 14:47
  • Not rather `std::unordered_map`? – Aconcagua Feb 04 '19 at 14:51
  • What's that kind of typedef syntax? GCC refuses to compile even with `-std=c++17`... – Aconcagua Feb 04 '19 at 14:53
  • @Aconcagua, I didn't know about `unordered_map`. Saw [this](https://stackoverflow.com/questions/2196995/is-there-any-advantage-of-using-map-over-unordered-map-in-case-of-trivial-keys) after your comment. Yes, probably `unordered_map` is a better option. – Duck Dodgers Feb 04 '19 at 14:54
  • @Aconcagua, I removed the `typedef`. p.s. I don't even have `C++11`. I can't even initialize that map with the initializer list. So I use static functions to initialize and typedefs to avoid re-typing in 3 different places. Sorry for that. – Duck Dodgers Feb 04 '19 at 14:56
  • Why not unordered_map? Which is what I meant by a hash map – Samer Tufail Feb 04 '19 at 14:59
  • I suspect that `N` would need to be surprisingly large for a linear search of a `std::vector>` to be slower than using either a `unordered_map` or `map`. Clang and recent GCC's can optimise it away to almost nothing, with no initial memory allocation, and all in contiguous memory. Both maps have a memory allocation per node, dislocated from the main data structure, and both will use a disproportionate amount of memory to the (small) strings. – marko Feb 07 '19 at 23:11
0

Assuming that you eventually want a std::string with the label in it, the question is whether to create them:

1: in to_string()
2: in its caller

Using Compiler Explorer it's pretty easy to find out.

Turns out (with recent compilers) that there's not a lot difference between the two. Returning const char * has a slight edge on std::string

1:

#include <string> 

char const * to_string(int code)
{
    switch (code)
    {
        case 0: return "foo";
        case 1: return "bar";
    }
}

std::string foo(int x)
{
    std::string s{to_string(x)};
    return s;    
}

2:

#include <string> 

std::string to_string2(int code)
{
    switch (code)
    {
        case 0: return "foo";
        case 1: return "bar";
    }
}


std::string foo2(int x)
{
    std::string s{to_string2(x)};
    return s;    
}

Note:

  • I needed to add foo() in order to stop the compiler optimising even more heavily....
  • In both cases, the strings are short and can use the short-string optimisation. Both clang and GCC have managed a heap-elision. This is seriously impressive - the compiler knows that to_string() never returns a string bigger than 4 bytes long and then eliminates the code that would dynamically allocate heap memory.

The conclusion seems to be that writing natural, and tidy code has little performance penalty.

Community
  • 1
  • 1
marko
  • 9,029
  • 4
  • 30
  • 46