Уточнить требования к аргументам стандартных type traits

Andrey
Andrey

Рассмотрим такой пример (https://gcc.godbolt.org/z/b_gtT7):

struct A {
    struct B {
        int i = 0 /* <-- @1 */;
    };

    struct C {
        C(int = 0 /* <-- @2 */);
    };

    static inline B b { }; // #1
    static inline C c { }; // #2

    static_assert(is_default_constructible_v<B>); // #3
    static_assert(is_default_constructible_v<C>); // #4
};

// #5

В точках #1 и #2 ошибка компиляции, потому что парсинг @1 и @2 откладывается до конца тела класса А (так как в точках @1 и @2 можно ссылаться на имена определенные в А ниже определения B). Соответственно static_assert-ы #3 и #4 падают, при этом очевидно, что is_default_constructible_v для B и С должны вычисляться в true в точке #5.

 

5
рейтинг
9 комментариев
Игорь Гусаров

Этот дефект присутствует ещё со времён C++98. И он проявляется не только в type traits:

struct Enclose
{
    struct Inner
    {
        Inner(int x = 3);
    };
    
    int     probe[sizeof((Inner()))];  // Error!
};
Игорь Гусаров
Andrey

Игорь Гусаров, считать ли дефектом, то что ваш код не компилируется, дело вкуса, лично я не вижу в этом проблемы, компилятор честно говорит, почему так происходит. Проблема же с моей точки зрения в том, что так как type traits кэширует результат вычисления, то была ли инстанциация внутри Enclose, влияет на то, какой результат мы получаем снаружи от Enclose. Т.е. ситуация аналогична применению type traits к incomplete типу.

Andrey
Игорь Гусаров

Andrey, то есть Вы хотите просто задокументировать нынешнее фактическое поведение? (точнее, неопределённость такого поведения)

Не хотите попробовать исправить такое поведение, добившись того, чтобы инстанциация type traits что внутри, что вне Enclose давала бы одинаковый результат? Просто на первый взгляд кажется, что в невычисляемом контексте не важно конкретное значение default argument, важно только то, что он задан. Соответственно, нет веских причин дожидаться окончания определения класса Enclose чтобы правильно инстанцировать тесты из type traits.

Игорь Гусаров
Обновлено 
Andrey

Игорь Гусаров, непонятно как обеспечивать корректность в таком случае:

struct Enclose
{
    struct Inner
    {
        Inner(int x = f());
    };
    
    static_assert(std::is_nothrow_default_constructible_v<Inner>);

    static int f() noexcept;
};

Нужно видеть, что `f()` объявлена как noexcept.

Andrey
Andrey

Игорь Гусаров, что касается хочу ли я задокументировать текущее поведение, то я бы хотел для начала понять, соответствует ли поведение gcc и clang-а на том примере, что вы привели нынешнему стандарту, и если нет, то надо бы поправить стандарт. Я задавал вопрос (процитирован ниже) в CWG mailing list, но там мне пока не ответили. Не разобравшись с этим к type traits приступать смысла действительно нет.

Both Clang and GCC give compilation errors for this code (https://gcc.godbolt.org/z/7Bdgkt) at lines #1 and #2.
 
struct A {
  struct B {
    int i = 0;
  };

  struct C {

    C(int = 0);
  };
 
  static inline B b { }; // #1
  static inline C c { }; // #2
};
 
I understand the reason for it, if my understanding is correct it's similar to http://wg21.link/p1286#existing-approach-is-bad-for-compilers.
But is there core wording explaining why this program shall be ill-formed?
Andrey
Игорь Гусаров

Andrey, как обеспечивать корректность - надо думать...

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

struct Enclose
{
    struct Inner
    {
        Inner() noexcept(flag);
    };

    std::vector<Inner>    m_data;
    // Ooops... std::vector may check
    // if Inner is nothrow constructible
    // to select the fastest implementation.
    // Now lots of type traits got instantiated.

    static const bool flag = true;
};

Это очень большие грабли, которые могут молча (без диагностики!) сломать логику программы.

Игорь Гусаров
Andrey

Игорь Гусаров, что касается "обеспечить корректность", мне кажется это невозможно.

А по поводу "молча (без диагностики!) сломать логику программы", я с вами совершенно согласен, это должно быть hard compiler error а не undefined behavior. Стандарт сейчас не запрещает вендорам (но и не требует) выдавать compiler error и по факту во многих случаях это делается для incomplete типов. http://cplusplus.github.io/LWG/lwg-defects.html#2797 как раз о том, чтобы потребовать диагностики в стандарте. Мое предложение в том, чтобы для nested классов с NSDMI, default arguments или noexcept-specification в конструкторах и операторах присваивания правила были как для incomplete типов.

Andrey
Игорь Гусаров

Andrey, мне интересна эта поднятая Вами проблема. По-видимому, Вы и сами ей вплотную занимаетесь, но если моё участие будет не лишним - скажите, помогу чем смогу с поисками и формулировками. Igor.Gusarov@kaspersky.com

Игорь Гусаров
Игорь Гусаров

Интересное наблюдение. Следующий пример был валидным вплоть до C++17. Потом в [dcl.fct.default]/6 добавили формулировку, что добавление дефолтного значения, которое превращает рядовой конструктор в какой-либо из специальных конструкторов после того, как класс был определён делает программу ill-formed:

struct Test
{
    Test(int arg);
};

// Test is NOT default constructible at this point.

Test::Test(int arg = 3)
{
}

// Test is default constructible at this point.

Хотя из тройки gcc / msvc / clang сейчас только clang выдаёт сообщение об ошибке. gcc/msvc молча компилируют приведённый пример.

Можно пойти по тому же пути, и потребовать, чтобы любые попытки изменить фундаментальные свойства класса после его закрывающей фигурной скобочки делали программу ill-formed с соответствующей диагностикой.

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