2013-03-05 2 views
24

Имеет ли следующий код (который выполняет арифметику указателей на границах подобъектов) корректное поведение для типов T, для которых он компилируется (что в C++ 11, does not not necessarily have to be POD) или любое его подмножество?Арифметика указателя на границах подобъектов

#include <cassert> 
#include <cstddef> 

template<typename T> 
struct Base 
{ 
    // ensure alignment 
    union 
    { 
     T initial; 
     char begin; 
    }; 
}; 

template<typename T, size_t N> 
struct Derived : public Base<T> 
{ 
    T rest[N - 1]; 
    char end; 
}; 

int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.initial == 10); 
    assert(&d.end - &d.begin == sizeof(float) * 10); 
    return 0; 
} 

LLVM использует изменение указанных выше методик при осуществлении внутреннего типа вектора, который оптимизирован для первоначального использовать стек для небольших массивов, но переключается на буфер кучи выделяется один раз по начальной емкости. (Причина делать это таким образом, не ясно, из этого примера, но, по-видимому, чтобы уменьшить шаблон разрастание кода, это яснее, если вы смотрите через code.)

ПРИМЕЧАНИЕ: Прежде чем кто жалуется, что это не совсем что они делают, и может быть, что их подход более соответствует стандартам, чем то, что я здесь дал, но я хотел спросить об общем случае.

Очевидно, что это работает на практике, но мне любопытно, если что-либо в стандартных гарантиях для этого. Я склонен сказать нет, учитывая N3242/expr.add:

Когда два указателя на элементы одного и того же объекта массива вычитаются, то результатом будет разность индексов двух элементов массива. .. Кроме того, если выражение P указывает на элемент объекта массива или один за последним элементом объект массива, а выражение Q указывает на последний элемент одного и того же объекта массива, выражение ((Q) +1) - (P) имеет то же значение, что и ((Q) - (P)) + 1 и as - ((P) - ((Q) +1)), и имеет значение 0, если выражение P точек один за последним элементом объекта массива, хотя выражение (Q) +1 не указывает на элемент объекта массива. ... Если оба указателя не указывают на элементы одного и того же объекта массива или один за последним элементом объекта массива, поведение не определено.

Но теоретически, средняя часть вышеприведенной цитаты, в сочетании с макетом класса и выравниванием гарантиями, может позволить следующему (незначительная) регулировки в силе:

#include <cassert> 
#include <cstddef> 

template<typename T> 
struct Base 
{ 
    T initial[1]; 
}; 

template<typename T, size_t N> 
struct Derived : public Base<T> 
{ 
    T rest[N - 1]; 
}; 

int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.rest[0] == 9); 
    assert(&d.rest[0] == &d.initial[1]); 
    assert(&d.rest[0] - &d.initial[0] == 1); 
    return 0; 
} 

, который в сочетании с различными другими положениями относительно union макет, конвертируемость и обратно от char * и т. д., возможно, также могут сделать исходный код действительным. (Основная проблема заключается в отсутствии транзитивности в определении арифметики указателя, приведенной выше.)

Кто-нибудь знает наверняка? N3242/expr.add, по-видимому, ясно указывает, что указатели должны принадлежать одному и тому же «объекту массива» для его определения, но может гипотетически быть в том случае, если другие гарантии в стандарте при объединении могут потребоваться определение в любом случае в этом случае, чтобы оставаться логически самосогласованным. (Я не ставка на него, но я бы это по крайней мере, возможно.)

EDIT: @MatthieuM вызывает возражение, что этот класс не является стандартной планировкой и, следовательно, не может быть гарантирован, не содержит отступов между базовый подобъект и первый элемент производного, даже если оба они выровнены с alignof(T).Я не уверен, насколько это верно, но это открывает следующий вариант вопроса:

  • ли это быть гарантированно работать, если наследование были удалены?

  • Будет ли гарантировано &d.end - &d.begin >= sizeof(float) * 10, даже если &d.end - &d.begin == sizeof(float) * 10 не было?

ПОСЛЕДНИЙ EDIT @ArneMertz выступает за очень внимательное прочтение N3242/expr.add (да, я знаю, что я читаю проект, но это достаточно близко), но делает стандарт действительно подразумевается, что следующее имеет неопределенное поведение, если линия подкачки удалена? (определения же класса, что и выше)

int main() 
{ 
    Derived<float, 10> d; 
    bool aligned; 
    float * p = &d.initial[0], * q = &d.rest[0]; 

    ++p; 
    if((aligned = (p == q))) 
    { 
     std::swap(p, q); // does it matter if this line is removed? 
     *++p = 1.0; 
    } 

    assert(!aligned || d.rest[1] == 1.0); 

    return 0; 
} 

Кроме того, если == не является достаточно сильным, что, если мы воспользуемся тем фактом, что std::less образует общий порядок над указателями, а также изменить условное выше:

if((aligned = (!std::less<float *>()(p, q) && !std::less<float *>()(q, p)))) 

Является ли код, предполагающий, что два равных указателя указывают на один и тот же объект массива, действительно нарушенный в соответствии со строгим чтением стандарта?

EDIT К сожалению, просто хочу добавить еще один пример, чтобы устранить стандартный вопрос компоновки:

#include <cassert> 
#include <cstddef> 
#include <utility> 
#include <functional> 

// standard layout 
struct Base 
{ 
    float initial[1]; 
    float rest[9]; 
}; 

int main() 
{ 
    Base b; 
    bool aligned; 
    float * p = &b.initial[0], * q = &b.rest[0]; 

    ++p; 
    if((aligned = (p == q))) 
    { 
     std::swap(p, q); // does it matter if this line is removed? 
     *++p = 1.0; 
     q = &b.rest[1]; 
     // std::swap(p, q); // does it matter if this line is added? 
     p -= 2; // is this UB? 
    } 
    assert(!aligned || b.rest[1] == 1.0); 
    assert(p == &b.initial[0]); 

    return 0; 
} 
+6

Не могу поверить, что в теге C++ есть хорошие вопросы. +1. –

+0

Возможно, дубликат [выравнивание элементов Союза] (http://stackoverflow.com/questions/891471/union-element-alignment), но я не уверен –

+0

@ BЈовић этот вопрос предполагает понимание ответа на этот вопрос, на самом деле –

ответ

8

Обновлено: Этот ответ на первый пропустил какую-то информацию и, таким образом, привести к неправильным выводам.

В ваших примерах initial и rest явно отчетливые (массив) объекты, поэтому сравнение указателей на initial (или ее элементы) с указателями на rest (или ее элементы) является

  • UB, если вы используете разница указателей. (§5.7,6)
  • неопределенные, если вы используете реляционные операторы (§5.9,2)
  • хорошо определен для == (Таким образом, второй отрезала хорошо, смотри ниже)

Первый фрагмент:

Построение разницы в первом фрагменте кода не определено поведение, для цитаты вы предоставили (§5.7,6):

если оба указатели POIN t к элементам одного и того же объекта массива или за последний элемент объекта массива, поведение не определено.

Для уточнения деталей UB первого примера кода:

//first example 
int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.initial == 10);   //!!! UB !!! 
    assert(&d.end - &d.begin == sizeof(float) * 10); //!!! UB !!! (*) 
    return 0; 
} 

Лини, обозначенна (*) интересно: d.begin и d.end не являются элементами одного и того же массива и, следовательно, результат операции в UB.Это несмотря на то, что вы можете reinterpret_cast<char*>(&d) и иметь оба их адреса в результирующем массиве. Но так как этот массив представляет собой все из d, это не видно, так как доступ к частям от d. Поэтому, хотя эта операция, вероятно, будет работать и даст ожидаемый результат в любой реализации, о которой можно мечтать, она по-прежнему является UB - как вопрос определения.

Второй фрагмент:

Это на самом деле хорошо определен поведение, но их реализация определяется результат:

int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.rest[0] == 9); 
    assert(&d.rest[0] == &d.initial[1]);   //(!) 
    assert(&d.initial[1] - &d.initial[0] == 1); 
    return 0; 
} 

линия, отмеченная (!) является не UB, но ее результат определяется реализация, поскольку прокладка, выравнивание и упомянутая установка могут сыграть свою роль. Но если это утверждение будет содержать , вы можете использовать две части объекта, как один массив.

Вы бы знали, что rest[0] будет класть сразу после initial[0] в память. На первый взгляд, вы не могли легко использовать равенство:

  • initial[1] хотел бы отметить одну пришедшего к концу initial, разыменования это UB.
  • rest[-1] явно выходит за пределы.

Но входит §3.9.2,3:

Если объект типа T расположен по адресу A, указатель типа резюмеT* значение которого является адрес A, как говорят, указывает на этот объект, независимо от того, как было получено значение. [Примечание: например, адрес, следующий за концом массива (5.7), будет считаться указывать на несвязанный объект типа элемента массива , который может быть расположен по этому адресу.

При условии, что &initial[1] == &rest[0], он будет двоичным, как если бы был только один массив, и все будет нормально.

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

Однако, есть некоторые оговорки: rest[-1] является UB, и так будет initial[2], из-за §5.7,5:

Если оба указателя операнд и точка результат для элементов одного и того же объект массива или один последний последний элемент объекта массива, оценка не должна приводить к переполнению; в противном случае это поведение undefined.

(акцент мой). Итак, как эти двое подходят друг другу?

  • «Хороший путь»: &initial[1] нормально, и с тех пор &initial[1] == &rest[0] вы можете взять этот адрес и перейти к увеличиваем указатель для доступа к другим элементам rest, из-за §3.9.2,3
  • " Bad path ": initial[2] is *(initial + 2), но так как §5.7,5, initial +2 уже UB, и вы никогда не сможете использовать §.3.9.2,3 здесь.

Вместе: вы должны остановиться у границы, сделать короткий перерыв, чтобы проверить, что адреса равны, а затем вы можете двигаться дальше.

+0

Я верю вам, но это означало бы невозможное выполнение ' std :: memset' или что-то подобное себе, но без вызова UB, независимо от того, был ли объект стандартным макетом или нет? Вы должны использовать предоставленные функции в качестве примитивов или еще? –

+0

(Кстати, мое редактирование было отклонено, но я думаю, что вы имеете в виду 'float *' not 'int *') –

+0

Я приму это, если вы поясните, считаете ли вы, что последний пример в моем отредактированном вопросе - UB или нет. (Может быть, технически, просто любопытно, что вы думаете.) –