встроенная функция в разных единицах перевода с разными флагами компилятора неопределенное поведение? - PullRequest
0 голосов
/ 28 августа 2018

в visual studio вы можете установить различные параметры компилятора для отдельных файлов cpp. например: в разделе «генерация кода» мы можем включить базовые проверки во время выполнения в режиме отладки. или мы можем изменить модель с плавающей запятой (точная / строгая / быстрая). это всего лишь примеры. Есть много разных флагов.

встроенная функция может быть определена в программе несколько раз, при условии, что определения идентичны. мы помещаем эту функцию в заголовок и включаем ее в несколько блоков перевода. Теперь, что произойдет, если разные параметры компилятора в разных файлах cpp приведут к немного другому скомпилированному коду для функции? тогда они отличаются, и у нас есть неопределенное поведение? Вы могли бы сделать функцию статической (или поместить ее в безымянное пространство имен), но в дальнейшем каждая функция-член, определенная непосредственно в классе, является неявной встроенной. это будет означать, что мы можем включать классы в разные файлы cpp, только если эти файлы cpp имеют одинаковые флаги компилятора. я не могу себе представить, что это правда, потому что это было бы легко ошибиться.

мы действительно так быстры в стране неопределенного поведения? или компиляторы будут обрабатывать эти случаи?

Ответы [ 2 ]

0 голосов
/ 30 ноября 2018

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

Нет. («Идентичный» здесь даже не является четко определенной концепцией.)

Формально определения должны быть эквивалентны в каком-то очень сильном смысле, который даже не имеет смысла как требование и о котором никто не заботится:

// in some header (included in multiple TU):

const int limit_max = 200; // implicitly static

inline bool check_limit(int i) {
  return i<=limit_max; // OK
}

inline int impose_limit(int i) {
  return std::min(i, limit_max); // ODR violation
}

Такой код вполне обоснован, но формально нарушает одно правило определения:

в каждом определении D, соответствующие имена, смотрел в соответствии с 6.4 [basic.lookup] должен ссылаться на объект, определенный в определении D, или должен ссылаться на тот же объект после перегрузки разрешение (16.3 [over.match]) и после сопоставления частичного шаблона специализация (17.9.3 [temp.over]), за исключением того, что имя может относиться к константный объект с внутренней связью или без нее, если объект имеет такую ​​же литеральный тип во всех определениях D, и объект инициализируется с постоянным выражением (8.20 [expr.const]), и значением (но не адрес) объекта используется , и объект имеет то же значение во всех определениях D;

Поскольку исключение не позволяет использовать объект const с внутренней связью (const int неявно статичен) с целью непосредственной привязки ссылки на const (а затем использовать ссылку только для ее значения). Правильная версия:

inline int impose_limit(int i) {
  return std::min(i, +limit_max); // OK
}

Здесь значение limit_max используется в унарном операторе + и , тогда константная ссылка привязывается к временному значению, инициализированному этим значением . Кто на самом деле это делает?

Но даже комитет не верит, что формальные ODR имеют значение, как мы видим в Core Issue 1511 :

1511. постоянные переменные и правило одного определения

Раздел: 6.2 [basic.def.odr] Статус: CD3 Отправитель: Ричард Смит Дата: 2012-06-18

[Перемещено в ДР на собрании в апреле 2013 года.]

Эта формулировка, возможно, недостаточно ясна для примера, подобного:

  const volatile int n = 0;
  inline int get() { return n; }

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

Важно то, что эффект встроенной функции является примерно эквивалентным: выполнение изменяемого чтения int, что является очень слабой эквивалентностью, но достаточной для естественного использования *1057* ODR, равного безразличие экземпляра: какой конкретный экземпляр встроенной функции не имеет значения и не может иметь значения .

В частности, значение, читаемое энергозависимым чтением, по определению не известно компилятору, поэтому условие post и инварианты этой функции, проанализированные компилятором, одинаковы.

При использовании разных определений функций в разных TU, вам нужно убедиться, что они строго эквивалентны с точки зрения вызывающего: что никогда нельзя удивить вызывающего, заменив одно на другое. Это означает, что наблюдаемое поведение должно быть точно таким же , даже если код отличается.

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

Потому что «стандарт» (который на самом деле не является спецификацией языка программирования) позволяет объектам с плавающей запятой иметь реальное представление, не допускаемое их официально объявленным типом, совершенно без ограничений, используя любые энергонезависимые квалифицированные плавающие Тип точки в любом множественно определенном субъекте для ODR кажется проблематичным, если только вы не активируете режим «double означает double» (который является единственным нормальным режимом).

0 голосов
/ 28 августа 2018

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

Даже при отсутствии встраивания рассмотрите возможность использования следующей функции в одном модуле компиляции:

char foo(void) { return 255; }

и следующее в другом:

char foo(void);
int arr[128];
void bar(void)
{
  int x=foo();
  if (x >= 0 && x < 128)
     arr[x]=1;
}

Если бы char был типом со знаком в обеих единицах компиляции, значение x во второй единице было бы меньше нуля (таким образом пропуская присвоение массива). Если бы это был тип без знака в обеих единицах, он был бы больше 127 (аналогично, пропуская назначение). Однако, если один модуль компиляции использовал подписанный char, а другой - без знака, и если реализация ожидала, что возвращаемые значения будут расширены до знака или расширены до нуля в регистре результатов, результатом может быть то, что компилятор может определить, что x не может быть больше 127, даже если оно содержит 255, или что оно не может быть меньше 0, даже если оно содержит -1. Следовательно, сгенерированный код может получить доступ к arr[255] или arr[-1], что может привести к катастрофическим последствиям.

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

...