Странное поведение с этой структурой мелкой копии?

#c

#c

Вопрос:

Я определил следующую структуру данных, целью которой является предоставление метода get_next(), который возвращает итератор, указывающий на следующую требуемую конфигурацию. Если required_explorations пусто или достиг конца, он возвращает его, указывая на начало.

 #include <iostream>
#include <map>
#include <string>
#include <unordered_map>

using configuration_model = std::unordered_map<std::string, std::string>;

struct doe_model {

    inline bool add_config(const std::stringamp; config_id,
        const configuration_modelamp; config,
        const int required_number_of_observations)
    {
        bool assignment_took_place = !required_explorations.insert_or_assign(config_id, config).second || !number_of_explorations.insert_or_assign(config_id, required_number_of_observations).second;
        next = required_explorations.end();
        return assignment_took_place;
    }

    inline void update_config(const std::stringamp; config_id)
    {
        number_of_explorations.at(config_id)--;

        // remove the configuration in case we exausted all the explorations
        if (number_of_explorations.at(config_id) <= 0)
            remove_config(config_id);
    }

    inline void remove_config(const std::stringamp; config_id)
    {
        required_explorations.erase(config_id);
        number_of_explorations.erase(config_id);
    }

    // this method returns the next configuration to explore
    // NOTE: the caller MUST check the pointer first.
    inline std::map<std::string, configuration_model>::iterator get_next()
    {
        // we may have an empty map or one with only a single configuration left
        if (required_explorations.empty() || next == required_explorations.end()) {
            std::cout << "Required explorations is empty or next at the end(), "
                         "returning the first pointer."
                      << std::endl;
            next = required_explorations.begin();
            return next;
        }

        next  ;

        std::cout << "Returning a next pointer normally." << std::endl;
        return next;
    }

    // key is the configuration_id
    std::map<std::string, configuration_model> required_explorations;
    std::map<std::string, configuration_model>::iterator next;
    std::map<std::string, int> number_of_explorations;
};
 

Я не настолько осведомлен о мелкой копии по умолчанию, что она генерируется для оператора присваивания с этой структурой, но, тестируя ее, я думаю, что проблема зависит от next копии итератора.

Выполнение этого возвращает 0 ожидаемый результат (который является первым элементом).

 int main()
{
    doe_model doe;
    doe.add_config("0", { { "threads", "29" } }, 4);
    doe.add_config("1", { { "threads", "23" } }, 4);
    doe.add_config("2", { { "threads", "20" } }, 4);
    doe.add_config("3", { { "threads", "21" } }, 4);
    doe.add_config("4", { { "threads", "22" } }, 4);

    auto it = doe.get_next();
    std::cout << it->first << "t" << std::endl;
}
 

Но выполнение следующего возвращает 4 , что является концом.

 int main()
{
    doe_model doe;
    doe.add_config("0", { { "threads", "29" } }, 4);
    doe.add_config("1", { { "threads", "23" } }, 4);
    doe.add_config("2", { { "threads", "20" } }, 4);
    doe.add_config("3", { { "threads", "21" } }, 4);
    doe.add_config("4", { { "threads", "22" } }, 4);

    doe_model new_doe = doe;

    auto it = new_doe.get_next();
    std::cout << it->first << "t" << std::endl;
}
 

Может кто-нибудь дать мне некоторое представление о том, что происходит за кулисами?

ОБНОВЛЕНИЕ вот реальный пример doe_model использования. Есть некоторые элементы данных и другие вызовы функций, о которых я не собираюсь говорить, поскольку они на самом деле не нужны для понимания того, как doe_model предполагается использовать. При send_configuration вызове ему необходимо извлечь новую конфигурацию из структуры doe, и для get_next этого необходим метод для выполнения этой задачи, возвращающий новую конфигурацию (отличную от предыдущей и которая все еще number_of_explorations > 0 существует).

   void send_configuration(const client_id_t amp;name)
  {
    if (num_configurations_sent_per_iteration <= num_configurations_per_iteration)
    {
      auto configuration = doe.get_next();
      if (configuration != doe.required_explorations.end())
      {
        remote->send_message(
            {MESSAGE_HEADER   "/"   app_id.app_name   "^"   app_id.version   "^"   app_id.block_name   "/"   name   "/explore",
             configuration_to_json(configuration->second)});


        doe.update_config(configuration->first);

        num_configurations_sent_per_iteration  ;
      }
    }
  }
 

Комментарии:

1. При копировании копируется doe базовая карта, итератор также копируется, но он указывает на doe.required_explorations , нет new_doe.required_explorations .

Ответ №1:

Элемент next; копируется, по-прежнему ссылаясь на другой экземпляр required_explorations .

При вызове get_next() сравнение next == required_explorations.end() имеет неопределенное поведение, оно определено только для итераторов в том же контейнере 1.

В стороне: в случае, когда required_explorations пусто, возврат required_explorations.begin() и разыменование также имеют неопределенное поведение.

  1. Если вы используете C 14 или более позднюю версию, вы можете сравнить свой итератор с итератором, инициализированным значением, который ведет себя end одинаково для всех контейнеров. В C 11 вам нужен второй итератор из контейнера, чтобы быть в безопасности.

Комментарии:

1. О, я понимаю.. Таким образом, определение пользовательского конструктора копирования и оператора присваивания должно быть правильным направлением, я полагаю. Или есть что-то проще, что можно сделать?

2. @Barnercart К сожалению, да, в общем случае. Или вообще избавиться от итераторов / ссылок.

3. @Barnercart Я бы посоветовал избавиться от next и get_next , заменив на begin / end , который раскрывает карту begin / end

4. @Caleth спасибо за совет. Я все равно планировал включить это в обзоры кода, как только это было bugs / unexpected_behaviours бесплатно.

5. @Caleth Я думал о вашем предложении, но мне нужно перебирать карту извне, и поскольку мне не нужно дважды обращаться к одному и тому же элементу, мне нужен метод ( next ), который дает мне возможность всегда обращаться к другому элементу (за исключением случаев, когда, конечно, есть только один элемент). Раскрытие begin/end не позволяет мне выбрать следующий элемент в «очереди» со стороны вызывающей стороны.