Как защитить значение локальной переменной после выхода из области видимости?

Я обернул функцию subscribe библиотеки C в метод класса C++ более высокого уровня. Внутри этого метода вызывается базовый C subscribe с локальной переменной char* (char key[10]), переданной в качестве ссылки. Проблема в том, что я только что понял, поскольку key - это локальная переменная, ее значение не защищено. Я могу передать его ссылку, но эта память будет освобождена, когда выйдет из области видимости. Я испытываю это поведение undefined, потому что обратный вызов никогда не вызывается - после отладки я увидел, что значение key изменилось.

Я пробовал использовать new char[10], который, похоже, работал. Но я думаю, это не тот путь, по которому я должен был идти.

Какое для этого правильное решение? Обновлять: исправлено заменой на string.

Обновлять

Функция интерфейса:

IoT_Error_t aws_iot_mqtt_subscribe(AWS_IoT_Client *pClient, const char *pTopicName, uint16_t topicNameLen,
                               QoS qos, pApplicationHandler_t pApplicationHandler, void *pApplicationHandlerData)

Обертка:

    std::function<void()> AWS::subscribe(const std::string &topic, std::function<void(const std::string&)> callback, QoS qos) {
  ESP_LOGI(TAG, "subscribe: %s", topic.c_str());

  std::string key("Test...");

  auto task = c_style_callback(
    [=] (AWS_IoT_Client *pClient, char *topicName, uint16_t topicNameLen, IoT_Publish_Message_Params *params) {
      std::string json;
      json.assign((char *)params->payload, (size_t)params->payloadLen);
      ESP_LOGI(TAG, "subscribe cb payload=%s", json.c_str()); // works
      ESP_LOGI(TAG, "key '%s'", key.c_str()); // undefined behaviour
      callback(key); // error, exit
      callback(json);
    }
  );

  m_error = ::aws_iot_mqtt_subscribe(
    &m_client,
    key.c_str(),
    key.length(),
    qos,
    task.get_callback<AWS_IoT_Client*, char*, uint16_t, IoT_Publish_Message_Params*>(),
    task.get_pvoid()
  );

  if (m_error != SUCCESS) {
    ESP_LOGD(TAG, "subscribe: error=%d", m_error);
    return nullptr;
  }

  return [=] () {
    ESP_LOGI(TAG, "unsubscribe %s", key.c_str());  // works
    callback(key); // works
  };
} // subscribe

Служебная функция c_style_callback:

template<class F>
struct c_style_callback_t {
  F f;
  template<class...Args>
  static void(*get_callback())(Args..., void*) {
    return [](Args...args, void* fptr)->void {
      (*static_cast<F*>(fptr))(std::forward<Args>(args)...);
    };
  }
  void* get_pvoid() {
    return std::addressof(f);
  }
};

template<class F>
c_style_callback_t< std::decay_t<F> >
c_style_callback( F&& f ) { return {std::forward<F>(f)}; }

Основная задача, при которой вызывается оболочка subscribe:

{
...
aws->subscribe(
  topic,
  [&] (const std::string &json) -> void {
    ESP_LOGI(TAG, "got json: %s", json.c_str());
  }
);
...
}

Обновление # 2

Лямбда обратного вызова внутри c_style_callback не имеет доступа к правильным значениям callback и key. Как защитить эти 2 от перезаписи? «Заворачивать» их в unique_ptr? Вернуть task звонящему для справки? Кроме того, возвращаемое значение помощника get_pvoid() указывает на пользовательские данные, которые являются лямбда-функцией, может быть, это следует защитить?

В C++ вы делаете это, передавая данные const. Пожалуйста, не используйте теги C и C++, если вы хотите получить ответ на C++.

tadman 13.09.2018 20:44

Тег C был удален.

haxpanel 13.09.2018 20:45

Скорее всего char key[10] в порядке. Обратитесь к документации по функции subscribe, чтобы узнать, какой тип указателя она ожидает.

NathanOliver 13.09.2018 20:45

Если ценность будет повреждена, сделайте копию.

tadman 13.09.2018 20:46

опубликуйте объявление функции интерфейса и образец кода, с помощью которого вы пытаетесь ее вызвать.

Richard Hodges 13.09.2018 20:46

@RichardHodges Спасибо, хорошо.

haxpanel 13.09.2018 20:48

Как правило, для таких ситуаций первым выбором является std::string (или string_view).

Jerry Coffin 13.09.2018 20:49

@JerryCoffin string_view полезно только в том случае, если часть интерфейса c содержит параметр размера. string_views не всегда заканчивается нулем.

Richard Hodges 13.09.2018 20:53

@RichardHodges: Хотя это более или менее верно, я не уверен, почему это особенно актуально. Я не просмотрел весь добавленный код, но, по крайней мере, в том виде, в котором он был опубликован, я не помню, чтобы видел достаточно информации, чтобы быть уверенным, будет ли string_view работать или нет.

Jerry Coffin 13.09.2018 21:03

Одно замечание к вашему коду: всякий раз, когда вы используете array new, вы наверняка делаете что-то не так. Вместо этого используйте std::vector. Даже использование простого нового кода обычно является признаком плохого кода.

Ulrich Eckhardt 13.09.2018 21:14

Спасибо, буду иметь ввиду.

haxpanel 13.09.2018 21:19
1
11
139
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Способ сделать это - выделить память в куче (new, malloc). У вас есть несколько вариантов:

  1. Предпочтительный выбор: Если вы new память, вы можете обернуть указатель в std::unique_ptr или std::shared_ptr и безопасно передать его. Когда последний экземпляр выходит за пределы области видимости, память автоматически освобождается. Прямой доступ к указателю можно получить с помощью get(). Вы должны убедиться, что никто другой не освобождает вашу память!
  2. Используйте тип RAII (Resource Acquisition Is Initialization) -wrapper, который выполняет malloc и free или new и delete. В этом случае вы выделяете память через конструктор и освобождаете ее через деструктор, одновременно передавая необработанный указатель на C-интерфейс. Имеет недостаток, заключающийся в том, что вам необходимо реализовать семантику копирования / перемещения, чтобы убедиться, что вы не выпускаете дважды, и что вы правильно отслеживаете все копии, которые хранят этот указатель. Из-за сложности, с которой это связано, я бы посоветовал вам использовать unique/shared_ptr, если вы можете каким-либо образом. Также можно передать настраиваемый удалитель на shared_ptr, так что вы также можете использовать его с free в качестве удалителя.
  3. Вы можете использовать необработанный указатель из new, но вы должны удалить его ровно один раз.

В этом есть смысл, я попробую предпочтительный способ. Спасибо!

haxpanel 13.09.2018 21:05

Голых new / delete следует избегать. Можно упомянуть std::make_unique / std::make_shared ..

Jesper Juhl 13.09.2018 21:12

@midor Какой, по вашему мнению, лучше всего подходит для этой проблемы? std::unique_ptr или std::shared_ptr? Не могли бы вы добавить какой-нибудь простой код для использования одного из них?

haxpanel 13.09.2018 21:23

@haxpanel Общее практическое правило: начните с наиболее строгого, std::unique_ptr, и ослабьте ограничения, в данном случае std::shared_ptr, только если это заставит. Подсказка в названии: shared_ptr следует использовать, если объект является общим. Если несколько других объектов имеют долю владения в объекте - объект не должен быть уничтожен, пока какой-либо из владельцев использует его - только тогда вы должны использовать shared_ptr. Несколько слов о концепции собственности, которые стоит прочитать.

user4581301 13.09.2018 21:46

если вам нужно сделать выбор в качестве возвращаемого значения функции, я бы выбрал unique_ptr, потому что его можно неявно преобразовать в shared_ptr.

midor 16.09.2018 22:04

Другие вопросы по теме