Вызов функции с указателем на неконстантный и указатель на константные аргументы с тем же адресом

14

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

Мне интересно, каков результат, если оба srcи dstуказали на один и тот же адрес, потому что я знаю, что компилятор может оптимизировать для const. Это неопределенное поведение? (Я отметил теги C и C ++, потому что я не уверен, что ответ может отличаться между ними, и я хочу знать об обоих.)

void f(const char *src, char *dst) {
    dst[2] = src[0];
    dst[1] = src[1];
    dst[0] = src[2];
}

int main() {
    char s[] = "123";
    f(s,s);
    printf("%s\n", s);
    return 0;
}

В дополнение к вышеприведенному вопросу, хорошо ли это определено, если я удалю constисходный код?

Уилли
источник

Ответы:

17

Хотя это правда , что поведение определено корректно - это не правда , что компиляторы могут «оптимизировать для сопзЬ» в том смысле , что вы имеете в виду.

То есть компилятору не разрешается предполагать, что только из-за того, что параметр является a const T* ptr, указанная память ptrне будет изменена через другой указатель. Указатели даже не должны быть равны. Это constобязательство, а не гарантия - ваше обязательство (= функция) не вносить изменения через этот указатель.

Чтобы действительно иметь такую ​​гарантию, вам нужно пометить указатель restrictключевым словом. Таким образом, если вы скомпилируете эти две функции:

int foo(const int* x, int* y) {
    int result = *x;
    (*y)++;
    return result + *x;
}

int bar(const int* x, int* restrict y) {
    int result = *x;
    (*y)++;
    return result + *x;
}

foo()функция должна прочитать дважды из x, в то время как bar()только нужно прочитать его один раз:

foo:
        mov     eax, DWORD PTR [rdi]
        add     DWORD PTR [rsi], 1
        add     eax, DWORD PTR [rdi]  # second read
        ret
bar:
        mov     eax, DWORD PTR [rdi]
        add     DWORD PTR [rsi], 1
        add     eax, eax              # no second read
        ret

Смотрите это в прямом эфире GodBolt.

restrictявляется только ключевым словом в C (начиная с C99); к сожалению, он до сих пор не введен в C ++ (по плохой причине, так как его сложнее ввести в C ++). Многие компиляторы поддерживают его, как, впрочем, и __restrict.

Итог: компилятор должен поддерживать ваш «эзотерический» вариант использования при компиляции f()и не будет иметь никаких проблем с ним.


Смотрите этот пост относительно вариантов использования для restrict.

einpoklum
источник
constне является «вашим обязательством (= функция) не вносить изменения через этот указатель». Стандарт C позволяет функции удалять constчерез приведение, а затем изменять объект в результате. По сути, constэто просто рекомендация и удобство для программиста, помогающее избежать непреднамеренного изменения объекта.
Эрик Постпишил
@EricPostpischil: это обязательство, которое вы можете выполнить.
einpoklum
Обязательство, из которого вы можете выйти, не является обязательством.
Эрик Постпишил
2
@EricPostpischil: 1. Вы раскалываете волосы здесь. 2. Это не правда.
einpoklum
1
Именно поэтому memcpyи strcpyобъявляются с restrictаргументами, а memmoveнет - только последний допускает перекрытие между блоками памяти.
Бармар
5

Это хорошо определено (в C ++, больше не уверен в C), с и без constквалификатора.

Первое, на что нужно обратить внимание - это строгое правило псевдонимов 1 . Если srcи dstуказывает на один и тот же объект:

Что касается constклассификатора, вы можете утверждать, что, поскольку, когда dst == srcваша функция эффективно изменяет то, srcна что указывает, srcне следует квалифицировать как const. Это не так, как constработает. Необходимо рассмотреть два случая:

  1. Когда объект определен const, как, например char const data[42];, его изменение (прямо или косвенно), приводит к неопределенному поведению.
  2. Когда ссылка или указатель на constобъект определены, например char const* pdata = data;, можно изменить базовый объект, если он не был определен как const2 (см. 1.). Таким образом, следующее четко определено:
int main()
{
    int result = 42;
    int const* presult = &result;
    *const_cast<int*>(presult) = 0;
    return *presult; // 0
}

1) Что такое строгое правило псевдонимов?
2) Является ли const_castбезопасно?

МКЦ
источник
Может быть, ОП означает возможность переупорядочения назначений?
Игорь Р.
char*и char const*не совместимы. _Generic((char *) 0, const char *: 1, default: 0))оценивает в ноль.
Эрик Постпишил
Фраза «Когда ссылка или указатель на constобъект определена» неверна. Вы имеете в виду, что когда определена ссылка или указатель на const-качественный тип , это не означает, что объект, на который он указывает, не может быть изменен (различными способами). (Если указатель действительно указывает на constобъект, это означает, что объект действительно constпо определению, поэтому поведение при попытке изменить его не определено.)
Эрик Постпишил
@Eric, я конкретен только тогда, когда речь идет о стандарте или теге language-lawyer. Точность - это ценность, которую я лелею, но я также знаю, что она усложняется. Здесь я решил пойти на простые и понятные предложения, потому что считаю, что это именно то, что хотел OP. Если вы думаете иначе, пожалуйста, ответьте, я буду среди первых, кто проголосует за это. В любом случае, спасибо за ваш комментарий.
YSC
3

Это хорошо определено в C. Строгие правила псевдонимов не применяются charни к типу, ни к двум указателям одного типа.

Я не уверен, что вы подразумеваете под "оптимизировать для const". Мой компилятор (GCC 8.3.0 x86-64) генерирует одинаковый код для обоих случаев. Если вы добавите restrictспецификатор к указателям, то сгенерированный код будет немного лучше, но это не сработает для вашего случая, указатели будут такими же.

(С11 §6.5 7)

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

В этом случае (без restrict) вы всегда получите 121результат.

СС Энн
источник