Clang 5: std:: дополнительные экземпляры для создания экземпляров std:: is_constructible признак типа параметра

Было обнаружено действительно странное и неожиданное поведение clang 5 при переходе на С++ 17 и замену стандартного решения std::optional на стандартное. По какой-то причине emplace() был отключен из-за ошибочной оценки черты std::is_constructible класса параметров.

Перед тем, как он воспроизведет, необходимо выполнить некоторые конкретные предпосылки:

#include <optional>

/// Precondition #1: T must be a nested struct
struct Foo
{
    struct Victim
    {
        /// Precondition #2: T must have an aggregate-initializer
        /// for one of its members
        std::size_t value{0};
    };

    /// Precondition #3: std::optional<T> must be instantiated in this scope
    std::optional<Victim> victim;

    bool foo()
    {
        std::optional<Victim> foo;

        // An error
        foo.emplace(); 
        /// Assertion is failed
        static_assert(std::is_constructible<Victim>::value);
    }
};

Пример Live на godbolt.org


Измените любое из предварительных условий и он скомпилируется, как и ожидалось. Есть ли какая-то неизвестная несогласованность в стандарте, которая заставляет clang отклонять этот код, будучи совместимым?

В качестве побочного примечания: GCC 7.1 и GCC 7.2 не имеют проблем с указанным выше кодом.


Отчет об ошибках: bugs.llvm.org

Ответы

Ответ 1

Это похоже на ошибку компилятора. Из [class]

Класс считается полностью определенным типом объекта (или полным типом) при закрытии } спецификатора класса.

Это означает, что Victim завершен в std::optional<Victim>, что делает его не иначе, как любой другой тип в этом контексте.

Из [meta]

Условие предиката для специализированной специализации is_­constructible<T, Args...> должно выполняться тогда и только тогда, когда следующее определение переменной будет хорошо сформировано для некоторой изобретенной переменной t: T t(declval<Args>()...);

Что такое прямая инициализация t с аргументами типа Args..., или если sizeof...(Args) == 0, это инициализация значения t.

В этом случае инициализация значения t - это значение по умолчанию-initialize t, которое верно, поэтому std::is_constructible_v<Victim> должно быть правда.

Со всем сказанным компиляторы, похоже, много борются, компилируя это.

Ответ 2

Хорошо, выкопали соответствующие цитаты. Суть в том, как std::is_constructible должен обрабатывать Victim. Наиболее убедительным является С++ 17 (n4659). Первый [meta.unary.prop/8]:

Условие предиката для типовой специализации is_­constructible<T, Args...> должно выполняться тогда и только тогда, когда следующее определение переменной будет хорошо сформировано для некоторых изобретенных переменная t:

T t(declval<Args>()...);

[Примечание: Эти токены никогда не интерпретируются как объявление функции. - end note] Проверка доступа выполняется как в контексте не связанных с T и любого из Args. Только действительность рассматривается контекст инициализации переменных.

Заметка, которую я выделил, не является нормативной (из-за записи), но она совпадает с [temp.variadic]/7

... Когда N равно нулю, экземпляр разложения дает пустой список. Такое создание не изменяет синтаксический интерпретация вложенной конструкции, даже в тех случаях, когда в противном случае исключение списка в противном случае было бы плохо сформированным или приводит к двусмысленности в грамматике.

Итак, для is_­constructible этот T t(); действительно делает t объявление переменной. Эта инициализация инициализируется значением, потому что [dcl.init/11] говорит так:

Объект, инициализатор которого представляет собой пустой набор скобок, т.е.(), должен инициализироваться значением.

Это означает, что этот признак заканчивается проверкой того, может ли Victim инициализироваться значение. Что это может быть. Это совокупность, но неявно дефолтный по умолчанию c'tor по-прежнему определяется компилятором (для поддержки инициализации значения, очевидно).

Короче говоря. У Clang есть ошибка, вы должны сообщить об этом.