Структуры и постоянные структуры

#c #struct #constants #typedef

#c #структура #константы #typedef

Вопрос:

У меня есть структура на C, которая представляет диапазон целых чисел:

 typedef struct {
    int* data;
    unsigned start;
    unsigned end;
} Range;
  

У меня есть одна функция, которая записывает значения в диапазон

 bool range_put(Range* const r, int value, unsigned idx)
{
    if(r->start <= idx amp;amp; idx < r->end)
    {
        r->data[idx] = value;
        return true;
    }
    else
    {
        return false;
    }
}
  

и одна функция, которая считывает данные из диапазона

 bool range_get(const Range* const r, int* const out, unsigned idx)
{
    if(r->start <= idx amp;amp; idx < r->end)
    {
        *out = r->data[idx];
        return true;
    }
    else
    {
        return false;
    }
}
  

Что я хотел бы сделать, так это расширить эти функции, чтобы я мог иметь диапазон до целых чисел const. Что-то вроде

 typedef struct {
    const int* data;
    unsigned start;
    unsigned end;
} ConstRange;
  

Как я могу заставить range_get() работать с обоими Range и ConstRange ?

Одним из решений было бы просто дублировать функциональность range_get() в новую функцию bool crange_get(const ConstRange*...) , но я хотел бы сохранить количество функций на низком уровне.

Другим решением было бы объединение Range (сомнительно, разрешено это или нет)

 typedef union {
    ConstRange const_range;
    struct {
        int* data;
        unsigned start;
        unsigned end;
    }
} Range;
  

Недостатком здесь является то, что синтаксис для вызова range_get() становится немного сложнее при передаче Range

 Range range;
...
range_get(amp;range.const_range ...);
  

Каковы другие альтернативы, снижающие количество функций и сложность использования функций?

 Range range;
ConstRange const_range;
...
range_get(amp;range ...);
range_get(amp;const_range ...);
  

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

1. Вы знаете, что если у вас есть a const int * , который указывает на некоторую память, вы не можете изменить или даже инициализировать память с помощью своего указателя. Если вы используете, например malloc , для выделения памяти, то вы застряли с неинициализированной памятью на все время ее существования. Решением может быть отслеживание того, какие элементы в диапазоне, которые были записаны один раз , и не позволяют записывать их снова. Это может быть достигнуто с помощью массива флагов, по одному флагу для каждого элемента в диапазоне. Что касается постоянной / неконстантной части, простого логического флага должно быть достаточно.

2. @JoachimPileborg «… тогда вы не сможете изменить …» — Вы не должны . const это гарантия, предоставляемая программистом, а не принудительная компилятором. Это просто позволяет компилятору предупреждать пользователя, но не мешает ему стрелять в ногу.

3. Вы могли бы использовать a union с двумя значениями. Используйте const int * для всех функций, кроме функций модификации.

Ответ №1:

С большим количеством функций проблем нет. Единственной (потенциальной) проблемой является дублирование кода. Итак, напишите не одну, а еще две функции:

Напишите новую функцию bool range_get2(const int* data, unsigned start, unsigned end, const r, int* const out, unsigned idx)

Затем сделайте свою существующую bool range_get(const Range* const r, int* const out, unsigned idx) работу, делегировав range_get2() . Обратите внимание, что это range_get2() будет принимать либо a const int* data , либо an int* data ; это не возражает.

Затем представьте свой новый bool crange_get(const ConstRange*...) и заставьте его также работать, делегировав range_get2() .

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

Ответ №2:

Как я могу заставить range_get() работать как с диапазоном, так и с ConstRange?

TL; DR: вы не можете, по крайней мере, напрямую.

const -квалифицированные типы не являются «совместимыми» с неквалифицированными const типами. Таким образом int , и const int не совместимы. Однако для некоторых целей имеет значение, что последняя является квалифицированной версией первой.

Два типа указателей, относящихся к несовместимым типам, несовместимы друг с другом, поэтому int * и const int * несовместимы. В этом случае ни одна const из версий, отвечающих требованиям a, не является версией другой.

Типы структур с разными тегами или члены которых с соответствующими именами не имеют совместимых типов, также несовместимы друг с другом, поэтому ваши Range и ConstRange несовместимы.

Поэтому вы не можете написать соответствующую программу, которая обращается к объекту типа ConstRange , как если бы он был типа Range , или наоборот. Это запрещено C2011, 6.5 / 7 («строгое правило сглаживания»). Действительно, даже не требуется, чтобы представления этих двух типов структур были расположены соответственно (хотя на практике они почти наверняка будут).

Вы также не можете получить доступ к объекту типа int * , как если бы он был типа const int * , или наоборот, хотя вы можете преобразовать значение одного из этих типов указателей в другой тип. Поведение такого преобразования четко определено в каждом случае, если новый тип не требует большего выравнивания, чем старый, что здесь было бы очень удивительно. И такое преобразование может быть полезным, потому что вам разрешено обращаться к an int как const int к — , например, путем разыменования a, const int * полученного путем преобразования из an int * .

Таким образом, вы можете написать соответствующие вспомогательные функции, которые служат общим фоном для чтения (но не изменения) data членов объектов ConstRange и Range , как предложил @MikeNakis , но вы не можете написать соответствующую функцию, которая принимает параметр одного из этих типов и каким-либо образом учитывает соответствующий аргумент, являющийсядругой тип.

Самое близкое, что вы могли бы сделать, это использовать объединение, но не совсем так, как вы представляете. Использование объединения по-прежнему не позволяет Range получить доступ к a, как если бы это было a ConstRange , и никакие ухищрения вокруг сноски 95 не обходят тот факт, что эти два не обязательно должны быть выложены соответственно, и что их data члены не имеют соответствующего типа. На практике такие махинации, скорее всего, приводят к ожидаемому результату, но программа, использующая их, тем не менее, не соответствует требованиям и полагается на неопределенное поведение.

Однако существует множество подходов, основанных на объединении, которые действительно работают, поскольку основаны на предоставлении способа установить, а затем вспомнить, какой член объединения является активным. Рассмотрим:

 typedef struct {
    union {
        int *data;
        const int *cdata;
    };
    unsigned start;
    unsigned end;
    _Bool is_const;
} Range;

_Bool range_get(const Range* const r, int * const out, unsigned idx) {
    if (r->start <= idx amp;amp; idx < r->end) {
        const int *data = r->is_const ? r->cdata : r->data;

        *out = data[idx];
        return true;
    } else {
        return false;
    }
}
  

Если вы настаиваете на том, чтобы иметь совершенно отдельный тип для диапазонов постоянных целых чисел, вы все равно можете сделать что-то подобное. Ключ в том, что вам нужно средство, чтобы указать функции, к какому члену объединения обращаться.

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

1. Обратите внимание, кстати, что я скопировал ваш стиль, сделав параметры r and out range_get равными be const , но это не служит никакой полезной цели (в отличие от того, чтобы r также быть указателем на const объект, который служит определенной цели).