Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Function taking a variadic template pack to convert std::strings to const char *?

Tags:

c++

formatting

I'm using a format function inspired by this answer. As long as I pass const char* into it, everything works just fine:

const char* hello = "Hello";
std::string world = "world";

string_format("%s, %s!", hello , world.c_str()); 
// Returns "Hello, world!"

Now, I am using std::strings everywhere and I'd like to avoid calling .c_str() everywhere. How can I modify this function to call it for me and to allow me to just pass std::strings into it?

like image 630
Valentin Avatar asked Mar 06 '23 19:03

Valentin


2 Answers

I ended up using an intermediate function safe:

#include <iostream>
#include <memory>
#include <iostream>
#include <string>
#include <cstdio>
#include <type_traits>


using namespace std;

template<typename T>
T safe(const T& value)
{return value;}

const char * safe(const string& value)
{return value.c_str();}


template<typename ... Args>
string string_format( const std::string& format, const Args& ... args )
{
    size_t size = snprintf( nullptr, 0, format.c_str(), safe(args)... ) + 1; // Extra space for '\0'
    unique_ptr<char[]> buf( new char[ size ] ); 
    snprintf( buf.get(), size, format.c_str(), safe(args)... );
    return string( buf.get(), buf.get() + size - 1 ); // We don't want the '\0' inside
}


int main()
{
    cout<<string_format("%s, %s! %d", "hello", string("world"), 42);

    return 0;
}
like image 58
Valentin Avatar answered Apr 27 '23 09:04

Valentin


You can add the .c_str() call to the parameter pack expansion inside the template function.

template<typename ... StringArgs>
std::string
string_format(const std::string& format, StringArgs ... args)
{
    std::size_t size = std::snprintf(nullptr, 0, format.c_str(), args.c_str() ...) + 1;
                                                               //^^^^^^^^^^^^
    std::unique_ptr<char[]> buf(new char[size]);
    std::snprintf(buf.get(), size, format.c_str(), args.c_str() ...);
                                                 //^^^^^^^^^^^^
    return std::string(buf.get(), buf.get() + size - 1);
}

int main(void)
{
    std::string h{"hello"};
    std::string w{"world"};
    std::cout << string_format("%s %s\n", h, w) << std::endl; // This works
    // This won't compile
    // std::cout << string_format("%d\n", 0) << std::endl;
}

This works because of how packs are expanded. From the docs:

A pattern followed by an ellipsis, in which the name of at least one parameter pack appears at least once, is expanded into zero or more comma-separated instantiations of the pattern, where the name of the parameter pack is replaced by each of the elements from the pack, in order.

There are several examples on that page of applying various transformations to each element in the pack, by doing things like func(pack)..., which is equivalent to func(item0), func(item1), ....


An obvious downside of this modified function is that it will not compile if the parameter pack is not all std::strings, because the .c_str() method is applied to every element of the parameter pack on expansion. You could probably figure out some trickery to keep both versions around, though.

like image 41
bnaecker Avatar answered Apr 27 '23 10:04

bnaecker