简体   繁体   中英

How to properly pass C strings to a lambda inside a variadic template function

Here is my complex situation:

I have a function using variadic template and lambda:

template<typename...Args>
void foo(Args...args) {
    // the arguments are passed to the lambda
    _func = [args...](){ do_sth(args...); };
}

On observing specific events, the lambda _func would be fired. My problem is, some of the arguments I passed are C strings, they could be pointing to temporary std string like this:

const char *pStr = __temp_std__string__.c_str();

Given I call foo(pStr); , and when _func is called, the temporary string that pStr is pointing to, has been released. I would like to know whether there exists a generic way to handle this. I am using C++11.

EDIT:

Perhaps I should post my whole story, as many of you advise to pass std::string instead of C strings, there are reasons that I can't escape from it.

I am developing games using cocos2d-x, which deploys C++11. What I want to do is to support auto-localisation of labels when players change their preferences of languages (selected from a UI).

I have saved the text in a couple of files, and each of them contains the localised text of a single language, they are basically under the following structure:

{
    "key1" : "_localized_text1_",
    "key2" : "_localized_text2_",
    ...
}

The idea is to observe the event on change of language's preference (through a notification), and I would get a key indicating that language from it, so as to fetch the localised text from the proper file. Here is the way how I implement it in the object class Label :

class Label {
    // this method would update the label's displayed text
    void setString(const std::string& text);

    // set a callback for changing language
    void setOnLanguageChangeFunc(std::function<void(Notification*)> func);

    // set a localised text, which would be updated on changing language
    void setLocalizeString(const std::string& key);
};

the core function is setLocalizeString (I skip the implementations of the other 2 methods as they are intuitive enough from their declaration):

void Label::setLocalizeString(const std::string& key) {
    // the callback lambda
    auto callback = [=](Notification *pNotification){
      setString(LOCALIZED_STRING(key)); 
    }

    // assign the lambda
    setOnLanguageChangeFunc(callback);
}

where LOCALIZED_STRING is the macro helper of fetching localised string with a key; and the lambda callback would be saved as local member variable of Label in setOnLanguageChangeFunc .

This works great in most cases, what makes the situation complicated is, there are format specifiers involved in the localised text, for example:

{
    ...
    "keyN" : "%s eats %d cookies",
    ...
}

Such format placeholders are passed dynamically in codes:

// formatStr = "Tom eats 5 cookies"
std::string formatStr = StringKit::stringWithFormat("%s eats %d cookies", "Tom", 5);

where StringKit is a utility to format the string, and it accepts variadic arguments which would be passed to vsnprintf to yield the output. Now you know why I need to pass C string and not std::string, its just due to the underlying method to format string.

Now I have to modify Label::setLocalizeString so that it could digest the possible variadic arguments:

template<typename... Args>
void setLocalizeString(const std::string& key, Args... args)
{
    // the callback lambda
    auto callback = [=](Notification *pNotification){
         setString(StringKit::stringWithFormat(LOCALIZED_STRING(sKey), args...)); 
    }

    // assign the lambda
    setOnLanguageChangeFunc(callback);
}

And this is its use case:

// on changing language, the label would display "Tom eats 5 cookies"
pLabel->setLocalizeString("keyN", "Tom", 5);

This case would work like a charm as that C string argument is global, but when it is passed from a temporary std::string :

std::string tempStr = "Tom";
pLabel->setLocalizeString("keyN", tempStr.c_str(), 5);

The C string "Tom" would lose the value on calling the lambda callback, since the pointed std::string, has been gone.

I have tried several ways, like playing with tuple things, or capturing a wrapper class of basic types in the lambda, but none of them could solve the problem. However, I think there should exist some tricky solutions.

This problem is not related to lambdas or variadic functions - it also occurs if you simply store the string:

const char* global_storage;

int main()
{
    {
        std::string s = "hi";
        global_storage = s.c_str();
    }

    // !!! `global_storage` points to deleted memory!
    use(global_storage);
}

You need to make sure the string lives long enough. Using std::string instead of const char* is a great starting point:

std::string global_storage;

int main()
{
    {
        std::string s = "hi";
        global_storage = std::move(s);
    }

    // OK, local string was moved into `global_storage`.
    use(global_storage.c_str());
}

If you really need to use a C-style string, just store it in the lambda/whatever as a std::string , then call .c_str() when you need to use it, not when storing it.

You need to convert your char const* arguments to std::string when storing it in lambda. This is one possible way, i can propose:

#include <iostream>
#include <tuple>
using namespace std;

template<typename T, typename R = conditional_t<is_same<T, char const*>::value, string, T>>
R bar (T &&value) {return value;}

template<class Ch, class Tr, class Tuple, std::size_t... Is>
void print_tuple_impl(std::basic_ostream<Ch,Tr>& os,
                      const Tuple & t,
                      std::index_sequence<Is...>)
{
    using swallow = int[]; // guaranties left to right order
    (void)swallow{0, (void(os << (Is == 0? "" : ", ") << std::get<Is>(t)), 0)...};
}

template<class Ch, class Tr, class... Args>
decltype(auto) operator<<(std::basic_ostream<Ch, Tr>& os,
                          const std::tuple<Args...>& t)
{
    os << "(";
    print_tuple_impl(os, t, std::index_sequence_for<Args...>{});
    return os << ")";
}

template<typename...Args>
decltype(auto) foo(Args...args)
{
    return [args = make_tuple(bar(args)...)] () { cout<< args; return; };
}

int main() {
    string *s = new string("Hello, World!");
    const char *p = s->c_str();
    auto f = foo(1, p, 3.14);
    delete s;
    f();
    return 0;
}

Function foo returns lambda that stores variadic arguments as tuple, where each char const* element is converted to std::string automatically. After that you can free temporary string. It's now should be safe to call that lambda after freeing.

IdeOne.com

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM