Запрет на использование массивов с передачей "по значению" в качестве типов параметров при объявлении функций.

Никита Колотов
Никита Колотов

С давних времен в С и С++ имеется специальное правило, по которому параметры функций с типом массив T на деле получают тип указатель на T: [dcl.fct] 11.3.5 Functions 5 ... After determining the type of each parameter, any parameter of type “array of T” or of function type T is adjusted to be “pointer to T”. Это правило выглядит неконсистентным даже на фоне прочих языковых неурядиц. Однако главное в том, что его использование приводит к опасному сочетанию "подразумевания" наличия у такого параметра свойств массива с о стороны разработчика с "попустительством" со стороны компилятора для которого это просто указатель. На практике это часто выливается в целый букет дефектов, как то:

1 закладывание на то, что массив действительно имеет указанный размер:

void foo(int items[10])
{
    for(size_t item_index{}; item_index < 10; ++item_index) // fail
    {
        cout << items[item_index];
    }
}
...
int items[5];
foo(items);

2 попытки получить размер массива "как обычно":

void foo(int items[100])
{
    size_t items_count{sizeof(items) / sizeof(items[0])}; // fail
    for(size_t item_index{}; item_index < items_count; ++item_index)
    {
        cout << items[item_index];
    }
}

3 игнорирование опциональности такого параметра:

void foo(int items[10])
{
    cout << items[0]; // fail
}
...
foo(nullptr);

4 промахи с вызовом перегрузок:

void foo(int poks[10], int raks[10])
{
    swap(poks, raks); // oops
}

5 неочевидная невозможность задать const квалификатор для такого параметра чтобы соблюсти const-correctness:

void foo(int const items[10])
{
    items = nullptr; // this is fine
}

В С возникающие проблемы по большей части такие же, хотя там может добавится дополнительная головная боль с VLA и использоваться несовместимый (и бесполезный) синтаксис со `static` размером `void foo(int items[static 10])`.

При этом я затрудняюсь найти хоть какие-то полезные применения для этого правила. Мне представляется, что все возможные сценарии можно без проблем реализовать переходом на передачу массива по ссылке / по указателю, либо на передачу просто сырого указателя (+ количество элементов),  либо на передачу array_view. Соответственно я предлагаю это правило заменить на прямой запрет таких действий: If function has parameter of type “array of T” the program is ill-formed.

 

11
рейтинг
11 комментариев
Andrey

Эта идея кажется неплохим кандидатом для C++ в новой эпохе, если эпохи будут когда-нибудь приняты. Есть ряд вопросов.

1. Почему компиляторы не дают warnings на void test(int arr[10]);? У них есть какие-то разумные причины -- если да, то какие, если нет, то почему бы не реализовать это для начала как warning?

2. Как это должно работать в dependent коде?

Case 1:

template<typename T>
void test(int arr[sizeof(T) - sizeof(int)]);

Case 2: 

template<typename T>
void test(typename T::type);

Если T::type это массив, он должен декэиться или давать SFINAE-error или hard error?

3. Стоит ли также запретить function type как параметр функии?

4. Не жалко ли совместимости с C?

Andrey
Никита Колотов

Andrey, 1. По аналогии с удалением триграфов, тут тоже вполне будет уместно разделить на две стадии - сначала объявление такого синтаксиса устаревшим, а затем уже полноценный запрет. Существующие компиляторы / статические анализаторы уже могут выдавать некоторые диагностики для подобных случаев, например warning c26485 в vc++ или cppcoreguidelines-pro-bounds-array-to-pointer-decay в clang-tidy.

2. Сase 1 - однозначно program ill-formed. А case 2 - SFINAE, сигнатура этой перегрузки просто не подходит, хотя среди прочих может найтись и подходящая.

Кстати с устранением этого правила вывода параметров шаблонов немного упростятся, особый пункт [temp.deduct.type] Except for reference and pointer types, a major array bound is not part of a function parameter type and cannot be deduced from an argument станет ненужным.

3. Да, я хотел создать для этого отдельное предложение. Хотя с указателями на функции проблем меньше.

4. Совместимость с С в этом плане и так только частичная: синтаксис с VLA и со `static` в С++ не поддерживается. А вообще было бы хорошо убрать аналогичное правило и из С.

Никита Колотов
Andrey

Я привел Case 1, потому что сейчас выражение в размере массива участвует в SFINAE: https://gcc.godbolt.org/z/jg--nU (правда не для GCC).

Не хотите все-таки начать с function type, я понимаю, что пользы от этого меньше, но с другой стороны это намного менее constroversial, т.е. можно будет посмотреть как в целом комитет по стандартизации реагирует на такого рода предложения, ломающие обратную совместимость?

Andrey
Никита Колотов

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

Никита Колотов
Andrey

Никита Колотов, кстати запрет decay для function type поможет выдавать более осмысленные сообщение об ошибках в случае ""the most vexing parse".

int x(int()); // ошибка тут, а не там где мы пытаемся работать с x как с переменной типа int.
Andrey
Никита Колотов

Andrey, хороший совет! Кстати в случае с параметрами-массивами most vexing parse тоже может всплыть:

#include <sstream>
#include <string>

int main(int, char** argv)
{
    std::stringstream ss(std::string(argv[1]));
}

argv тут распарсится как имя параметра-массива в объявлении функции ss

Никита Колотов
Никита Колотов

По-каким-то причинам редактировать предложения нелязя, так что дополню тут. К п.5 можно придумать более вопиющий пример:

using items_t = int const [10];

void test(items_t const items) // const ignored
{
    items = 0; // wat
}
Никита Колотов
Никита Колотов

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

void foo(int * arr);
void foo(int arr[2]);
void foo(int arr[142]);
void foo(int arr[10]){}
Никита Колотов
Обновлено 
Игорь Гусаров

1. Кажется, std::array<> уже решает все описанные проблемы.

2. Запрет на использование какого-то типа в качестве типа аргумента функции может больно ударить по обобщённому шаблонному коду, поскольку потребуется отдельно учитывать этот особый тип в отдельной перегрузке / специализации. Мне кажется, что это не очень хорошее следствие.

Игорь Гусаров
Никита Колотов

Игорь Гусаров, Опять же, вы явно неверно истолковали смысл этого предложения. Массивы (и функции, о чем идет речь во второй части этого предложения) ни в С, ни в С++ и так не могут быть типом аргумента функции. В этом плане ничего не меняется. Данные предложения направлены на отмену специальных правил толкования таких типов когда они фигурируют в объявлении функий.

Никита Колотов
makaleks

После введения C++20 (конкретно - class non-template parameters) это предложение сделает невозможным использовать строковые литералы в constexpr-конструкторах. Конечно, в конструктор можно передать сразу std::array, но сейчас этот std::array не нужно инициализировать отдельно, а значит - фрагментировать код, делать его менее читаемым. Например, сейчас (в gcc) можно писать следующее, и мне это нравится:

// Программа

int main() {
    // Вместо этого
    constexpr auto mess = MyMessyClass<"123e4567-e89b-12d3-a456-426655440000">();
    // придётся использовать std::array<char,37> и инициализироваться как-то так:
    //   constexpr auto mess = MyMessyClass<
    //       std::array<char,37>{{"123e4567-e89b-12d3-a456-426655440000"}}
    //   >();
    // Конечно, можно сократить размер с помощью алиасов, но новые сущности
    // не улучшат читаемость
}

// Библиотека

class Uuid {
    std::uint8_t data_[16] = {0};
public:
    // Станет невозможным, если запретить массив как аргумент
    constexpr Uuid (const char input[37]) {
        // ...
    }
};

// C++20
template <Uuid uuid>
class UuidDefined;

struct SomeBaseInterface {
    virtual void f1 () = 0;
    virtual void f2 () = 0;
};

class UuidDefined <"123e4567-e89b-12d3-a456-426655440000"> : public SomeBaseInterface {
    void f1 () {
        // ...
    }
    void f2 () {
        // ...
    }
};

// ...

template <Uuid uuid>
class MyMessyClass {
    UuidDefined<uuid> someProvider;
    static_assert_v(std::is_convertible<UuidDefined<uuid>*, SomeBaseInterface*>::value, "someProvider must inherit SomeBaseInterface")

    // И т.д.:
    // UuidDefined<uuid> networkProvider;
    // UuidDefined<uuid> guiProvider;
    // UuidDefined<uuid> pluginProvider;
};

На другой недостаток - отбрасывать код на Си - уже указывали.

makaleks
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).
Все предложения