Почему Valgrind сообщает о «неинициализированном значении», но не о «недопустимом чтении» при доступе к векторам C++?

Я работаю над программой на C++, которая включает доступ к элементам std::vector . Я столкнулся с проблемой при запуске моего кода с помощью Valgrind, когда он сообщает об ошибке «неинициализированное значение», а не об ошибке «недопустимое чтение», как я ожидал. Вот упрощенная версия моего кода:

#include <vector>
#include <iostream>
#define DATA_SIZE 10

struct Data {
  Data(double aa, double bb) : a(aa), b(bb) {}
  double a {0.0};
  double b {0.0};
};

class Test {
  public:
    Test() {
      data_.clear();
      data_.reserve(DATA_SIZE * 2);
      for (int i = 0; i < DATA_SIZE; ++i) {
          data_.push_back(Data(i, i));
      }
    }
    double Read() {
      const int index = DATA_SIZE;
      const auto& data = data_[index];
      double res = data.a + data.b;
      std::cout << res;
      return res;
    }
  private:
    std::vector<Data> data_;
};

int main() {
  Test t;
  t.Read();
}

Machine: Linux Ubuntu 18.04 с g++ 7.5 и valgrind 1:3.13.0-2ubuntu2.3

Compile

g++ -std=c++17 -O2 -g test.cpp -o test

Run

valgrind --tool=memcheck --leak-check=full --expensive-definedness-checks=yes --track-origins=yes ./test

Результат

Валгринд сообщил о большом количестве Conditional jump or move depends on uninitialised value(s) на operator<<(std::cout << res;).

Вопросы:

Я ожидал, что Valgrind сообщит о «недопустимом чтении», поскольку доступ выходит за пределы, но он сообщает только об ошибке «неинициализированного значения». Почему Valgrind сообщает «неинициализированное значение», а не «недопустимое чтение»?

Дополнительная информация:

Интересно, если я изменюсь
От data_.reserve(DATA_SIZE * 2); до data_.reserve(DATA_SIZE); Valgrind сообщает о «недопустимом чтении» (как и ожидалось). Я чувствую, что проблема заключается в размере векторных резервов, но я не совсем понимаю, почему/что это такое.

Я не эксперт, но насколько я понимаю, "недопустимое чтение" означает, что вы обращаетесь к памяти, которая вам не принадлежит. В данном случае память принадлежит вам, но инициализированного объекта там нет.

Yksisarvinen 30.08.2024 12:20

Насколько я понимаю, вы вызываете резерв для элементов DATA_SIZE * 2, но не инициализируете их все. Правильно ли я понимаю @Yksisarvinen?

kiner_shah 30.08.2024 13:16

@kiner_shah Цикл ниже инициализирует элементы по индексам от 0 до DATA_SIZE-1, затем в основном они пытаются прочитать индекс DATA_SIZE (первый неинициализированный). Код как есть, он читает из допустимой памяти, но без допустимого объекта. Когда OP изменяется reserve, вызывается только резерв DATA_SIZE, чтение происходит из недействительной памяти.

Yksisarvinen 30.08.2024 13:24

Это не выходит за рамки, поскольку у вас есть место (как минимум) для DATA_SIZE * 2 элементов. Первое «недопустимое чтение» находится в data[data.capacity()], а не в data[data.size()].

molbdnilo 30.08.2024 13:39

@Yksisarvinen, такое же понимание. Спасибо за разъяснения.

kiner_shah 30.08.2024 14:38
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
68
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Memcheck работает на гораздо более низком уровне, чем исходный код высокого уровня. Он ничего не знает о std::vector. Все, что он видит, — это выделения (с оператором new), а также чтение и запись (а также системные вызовы). Поэтому, когда вы пишете data_.reserve(DATA_SIZE * 2);, memcheck перенаправит базовый вызов на оператора new и запишет, что 320 байт были выделены с выравниванием по умолчанию. Внутри он выделит теневую память, помеченную как неинициализированную.

Когда вы проходите

for (int i = 0; i < DATA_SIZE; ++i) {
          data_.push_back(Data(i, i));
      }

нижние 160 байтов помечаются как инициализированные. Затем, когда вы читаете

      const int index = DATA_SIZE;
      const auto& data = data_[index];

это все еще находится в пределах блока размером 320 байт, поэтому оно доступно. Но теневая память указывает, что она неинициализирована. Однако ошибок пока нет, memcheck позволяет копировать неинициализированную память.

Когда вы доберетесь до своего cout, вы получите ошибку. Во время преобразования из типа double в какую-либо строку будет выполняться несколько условий (отрицательна ли она? Отрицательна ли показатель степени? Является ли показатель степени больше 1e6 и так далее). Эти проверки условий для неинициализированных значений вызовут ошибки. (Возможно, это не те места, где возникают ошибки, это просто пример).

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