Поврежденные одноэлементные данные с использованием CxxTest

Это странная проблема, и я не уверен, что с этим делать.

У меня есть что-то вроде следующего:

struct Parms
{
    const std::string value1;
    const std::string value2;

    std::string parm1;
    std::string parm2;

    Parms() : parm1(value1), parm2(value1) {}

    static const Parms& getDefaults()
    {
        static Parms defaults;
        return defaults;
    }
};

Который я обычно использую так:

Parms myParms = Parms::getDefaults();
myParms.parm1 = "crap";
functionThatNeedsParms(myParms);

Довольно просто. Это никогда не вызывало у меня головной боли, пока я не начал писать модульные тесты, использующие этот код, используя CxxTest. У меня есть два класса набора тестов в разных файлах, и когда я запускаю их по отдельности, все отлично.

Когда я запускаю их вместе, я вижу две плохие вещи. Во-первых, все это ядро ​​сбрасывает, пытаясь дважды освободить статическую переменную по умолчанию. Во-вторых, если я посмотрю на содержимое defaults за некоторое время до того, как он умрет, но после того, как я начал его использовать, статические константы std::strings, которые там находятся, повреждены (некоторые буквы случайно изменились, хотя это всегда одно и то же при каждом запуске).

Что здесь происходит?


person Paul D.    schedule 31.08.2010    source источник
comment
Является ли код таким же, как указано выше (т. е. getDefaults неявно встроен из-за определения внутри класса). Параметры встроены в одну библиотеку, а весь тестовый код встроен в другую библиотеку? Являются ли value1/2 просто константными членами, а не ссылками, статическими или чем-то еще странным.   -  person Martin York    schedule 31.08.2010
comment
«по умолчанию» — не очень хорошее название. это очень похоже на «по умолчанию», которое является ключевым словом C++.   -  person Chubsdad    schedule 01.09.2010
comment
Почему в посте написано Синглтон? Похоже, что этот класс делает гораздо больше вещей, чем просто функциональность «Singleton».   -  person Chubsdad    schedule 01.09.2010


Ответы (3)


двойное освобождение и дамп ядра

Я думаю, что могу объяснить проблему «двойного бесплатного и основного дампа», с которой вы столкнулись. Недавно я столкнулся с тем же самым, и похоже, что вы делаете то же самое, что и я.

Из вашего описания вы сказали, что когда вы «запускаете их по отдельности», они работают нормально, но если вы «запускаете их вместе», вы получаете двойную проблему с дампом ядра.

Я обнаружил, что это происходит, если один и тот же глобальный объект объявляется дважды.

В моем случае у меня был класс foo, в одном файле у меня был глобальный class foo gFoo;, а в другом файле у меня был глобальный class foo gFoo;. (Да, это звучит глупо, на самом деле я связывался с файлом X.cxx, а также с общей библиотекой, которая также включала X.cxx — результаты были практически одинаковыми.)

Теперь я ожидал, что компилятор пожалуется на это, но, видимо, есть флаги для включения или отключения этой проверки, и код скомпилирован нормально. Но когда программа завершала работу и вызывала все свои деструкторы, она дважды вызывала деструкторы gFoo и выдавала мне двойное бесплатное сообщение вместе с дампом ядра.

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

Проверь это.

person John Rocha    schedule 01.09.2010

статические переменные в C и C++ не являются потокобезопасными. Это означает, что условия гонки (плохая вещь) могут возникнуть, если два потока попытаются получить доступ к вашему одноэлементному объекту. Один из подходов к решению вашей проблемы — использовать локальное хранилище потоков. Это поддерживается библиотекой pthreads, а также некоторые компиляторы напрямую поддерживают локальное хранилище потоков.

Альтернативой, если ваш синглтон ДОЛЖЕН быть глобальным для всех потоков, является предоставление блокировки, чтобы гарантировать, что только один поток может получить доступ к вашим данным за раз.

Однако проблема возникает только при модульном тестировании. Я бы посоветовал не запускать многопоточные модульные тесты, если вы не собираетесь использовать свой синглтон в нескольких потоках.

person doron    schedule 31.08.2010
comment
Я не вижу в вопросе ссылки на многопоточную среду, поэтому этот ответ цепляется за соломинку. Вы должны были опубликовать комментарий к вопросу о многопоточности, прежде чем дать ответ. - person Martin York; 31.08.2010
comment
Я предполагаю, что по характеру проблемы и фразе, которая работает вместе, это на самом деле многопоточность. Я могу ошибаться, но в вопросе мало деталей, и это может быть проблемой. - person doron; 31.08.2010

Это сильно зависит от используемого вами компилятора и платформы, и, не видя тестов, я могу только догадываться о том, что происходит.

Я вижу некоторые неправильные представления в вашем коде:
1) Вам не хватает оператора копирования и конструктора копирования. Вы копируете экземпляры, содержащие std::string, которые могут быть реализованы с использованием подсчета ссылок. Подсчет ссылок реализован в перегруженном конструкторе/операторе копирования std::string, но они могут не вызываться из неявно сгенерированного конструктора копирования вашего класса и, следовательно, вызывать двойное освобождение памяти и другие неприятные вещи. Оператор/конструктор копирования должен выглядеть так:

// Copy constructor
Parms(const Parms& oth)  { parm1 = oth.parm1; parm2 = oth.parm2; }

// Copy operator
Parms& operator= (const Parms& oth)  { 
  if (&oth == this)  // Check for self-assignment
    return *this;
  parm1 = oth.parm1;
  parm2 = oth.parm2;
  return *this;
}



2) Я не совсем понимаю наличие value1 и value2. Кажется, вы никогда их не инициализируете, вы просто используете их в конструкторе по умолчанию, чтобы скопировать их (пустое) содержимое в parm1 и parm2. Вы можете полностью избежать этого, так как parm1 и parm2 автоматически инициализируются пустой строкой при вызове.

3) Здесь не нужно использовать шаблон singleton. Метод getDefaults() может быть реализован следующим образом:

static Parms getParms() { return Parms(); }

Шаблон singleton предназначен для классов, у которых есть только один экземпляр во время выполнения всей программы, что, похоже, не относится к вашему классу.

Таким образом, вы можете безопасно использовать функцию getParms() из нескольких потоков, и умный компилятор оптимизирует подразумеваемую дополнительную копию.

person Karel Petranek    schedule 31.08.2010
comment
Извините, я должен был быть более точным в своем первоначальном вопросе. Это проблема, с которой я столкнулся на работе, поэтому я не собирался копировать и вставлять фактический код, поэтому то, что я собрал, было (по-видимому, не таким уж ясным) тривиальным примером. Поведение синглтона используется, потому что есть также метод setDefaults(), поэтому вы можете установить их один раз, а затем повторно использовать где угодно. Во всяком случае, настоящая проблема была № 1. Я не осознавал, что в реализации std::string используется подсчет ссылок, поэтому я предположил, что поверхностная копия будет допустимой. Спасибо за ваш ответ. - person Paul D.; 31.08.2010
comment
На самом деле подождите секунду. Почему его пересчет взорвет все? Если я вызову getDefaults() и назначу его какому-то типу Parms, он должен просто увеличить количество ссылок, а затем позже, когда тест закончится и он выйдет за рамки, он должен уменьшить его, и жизнь должна продолжаться как обычно. . - person Paul D.; 31.08.2010
comment
О, я вижу, что вы говорите. Есть ли какая-то окончательная спецификация, чтобы посмотреть на все возможные конструкторы копирования и операторы присваивания? Мне кажется странным, что они разрабатывают вещи таким образом, что вы можете случайно сделать недействительным счетчик ссылок. - person Paul D.; 31.08.2010
comment
Да, это будет работать, если оператор копирования по умолчанию вызывает соответствующий оператор копирования строки. Несмотря на то, что это должно быть так (согласно stackoverflow.com/questions/2009996/) Я обнаружил, что некоторые компиляторы ведут себя неправильно и делают эквивалент memcpy() (я думаю, что Visual C++ 6 сделал это, и было довольно сложно это отследить). - person Karel Petranek; 31.08.2010
comment
Еще одна мысль: если у вас также есть функция setDefaults, вы делаете переменную Parms defaults статическим членом класса? Подсчет ссылок может быть проблемой, а может и не быть, мне нужно увидеть полный код, чтобы сделать более точные предположения :) - person Karel Petranek; 31.08.2010
comment
1) Нет необходимости иметь конструктор копирования или оператор присваивания, поскольку класс не управляет ресурсами, и поэтому значения по умолчанию совершенно безопасны. 2) Даже если используется подсчет ссылок; std::string никогда не приведет к двойному удалению. 3) Это ужасный оператор присваивания (он не обеспечивает строгой гарантии исключения (пожалуйста, посмотрите Copy and Swap Idiom)) - person Martin York; 31.08.2010
comment
@Marting York Спасибо, что заметили недостатки. Рекламные пункты 1 и 2: как я отметил в комментариях, у меня были проблемы, когда у меня был класс без оператора копирования, который содержал std::string (хотя и с древними компиляторами). Несмотря на то, что это может быть редкостью, учитывая небольшое количество предоставленной информации, я решил упомянуть об этом. Ad пункт 3: Здесь вы совершенно правы. - person Karel Petranek; 31.08.2010