SFINAE-дружественный тест для удаления объектов - PullRequest
0 голосов
/ 14 ноября 2018

Меня интересует программное определение, является ли delete-expression допустимым для некоторого данного объекта. Похоже, что это можно сделать с помощью SFINAE. Приведенный мною код работает так, как я ожидаю с Clang 6.0.1, но не скомпилируется с GCC 7.3.0.

Два вопроса: 1) есть ли лучший способ достичь цели, и 2) кто прав?

#include <type_traits>

using namespace std;

// C++14 variant of void_t
template< typename ...T > struct void_type { typedef void type; };
template< typename ...T > using void_t = typename void_type<T...>::type;

template< typename T, typename = void_t<> > struct Deletable : false_type { };
template< typename T >
struct Deletable<T, void_t<decltype(delete declval<T>())> > : true_type { };
//        Here's the SFINAE'd test  ^^^^^^^^^^^^^^^^^^^

struct A {
    operator signed *() const;
};

struct B {
    operator signed *() const;
    operator unsigned *() const;
};

int main() {
    static_assert(Deletable<A>::value, "Couldn't delete A objects but "
        "should be able to.");
    static_assert(!Deletable<B>::value, "C objects are deletable but "
        "shouldn't be because pointer-to-object is ambiguous.");
    return 0;
}

Моя попытка ответить на вопрос 2

tl; dr: Я думаю, что Clang прав. Перейдите к нижней части для нескольких других тестовых случаев.

Вот почему, используя N4140 .

Соответствующий раздел о выражении удаления , когда ему присваивается тип класса, читает [expr.delete] стр.1:

[...] Если тип класса, операнд [ delete-expression ] контекстно неявно преобразуется в указатель на тип объекта, delete-expression 's результат имеет тип void.

Обратите внимание, что void * не является "указателем на объект". Здесь мы находим, что результат decltype(delete <blah>) равен void: это не зависимое выражение. Тем не менее, я думаю, что для SFINAE нас просто интересует, правильно ли это сформировано.

Преобразование выполняется в соответствии с [conv] стр.5:

Выражение e класса E, фигурирующее как [операнд delete-expression ]: называется контекстно неявно преобразованным в указанный тип T и правильно сформирован тогда и только тогда, когда e может быть неявно преобразован в тип T, который определяется следующим образом: E ищется для преобразования функции, тип возврата которых cv T или ссылка на cv T, такая что T разрешено контекстом. Должен быть ровно один такой T.

Поскольку T должен быть указателем на объект, мы видим, что класс A соответствует критериям преобразования (и поэтому может быть удален), предоставляя единственную функцию преобразования в указатель на объект, тогда как B , предоставляя две такие функции, не имеет. Таким образом, попытка удалить объект B должна привести к неправильному выражению. Это то, что static_assert s в приведенном выше коде должны обеспечить.

Возможный альтернативный подход: можно ли проверить объект на «конвертируемость в некоторый указатель на тип объекта»?

Продолжаем, вот определение недопустимого выражения с целью подстановки типа [type.deduct] стр.8:

Если подстановка приводит к неверному типу или выражению, введите вычет не удается. Недопустимый тип или выражение - это то, что плохо сформирован, с необходимостью диагностики, если написано с использованием замещенные аргументы. [...] Только недопустимые типы и выражения в непосредственном контексте типа функции и ее типов параметров шаблона могут привести к ошибке вывода. [Примечание: оценка замещенных типов и выражений может привести к побочным эффектам, таким как создание специализаций шаблонов классов и / или специализаций шаблонов функций, генерация неявно определенных функций и т. Д. Такие побочные эффекты отсутствуют в «непосредственном» контекст »и может привести к тому, что программа будет плохо сформирована. - конец примечания]

Насколько я могу судить, поскольку удаление объекта B неправильно сформировано (с необходимостью диагностики), выражение decltype(delete <b>) должно быть недопустимым, что приводит к срабатыванию SFINAE. Deletable затем заканчивается наследованием от false_type, и мы золотые.

С Клангом, похоже, так и есть. С GCC вот результат:

main.cpp: In substitution of ‘template<class T> struct Deletable<T, typename
    void_type<T, decltype (delete (declval<T>)())>::type> [with T = B]’:
main.cpp:24:32:   required from here
main.cpp:24:32: error: ambiguous default type conversion from ‘B’
    static_assert(!Deletable<B>::value, "Shouldn't be able to delete B objects "
                               ^~
main.cpp:24:32: note:   candidate conversions include ‘B::operator int*()
    const’ and ‘B::operator unsigned int*() const’
main.cpp:24:32: error: type ‘struct B’ argument given to ‘delete’, expected
    pointer

Я предполагаю, что GCC не рассматривает контекстное неявное преобразование как часть непосредственного контекста, поэтому неоднозначность становится серьезной ошибкой, а не просто ошибкой вывода типа. Учитывая очевидное намерение «непосредственного контекста», как определено в примечании выше, я думаю, что преобразование должно быть в непосредственном контексте. Поэтому, как и выше, я думаю, что Clang прав.

For полнота, другие возможные причины, по которым объект не может быть удален:

class C {
    ~C();
public:
    operator C*() const;
};

struct D {
    ~D() = delete;
    operator D*() const;
};

struct E {
    operator E*() const;
private:
    void operator delete(void*);
};

int main() {
    static_assert(!Deletable<C>::value, "C objects are deletable but shouldn't "
        "be because destructor is private.");
    static_assert(!Deletable<D>::value, "D objects are deletable but shouldn't "
        "be because destructor is deleted.");
    static_assert(!Deletable<E>::value, "E objects are deletable but shouldn't "
        "be because deallocation function is inaccessible.");
    return 0;
}

В Clang все работает нормально, и, что интересно, static_assert() для D проходит для GCC, однако случаи C и E завершаются неудачей.

main.cpp: In instantiation of ‘struct Deletable<C>’:
main.cpp:31:32:   required from here
main.cpp:11:76: error: ‘C::~C()’ is private within this context
 struct Deletable<T, void_t<T, decltype(delete declval<T>())> > : true_type { };
                                                                            ^
main.cpp:14:5: note: declared private here
     ~C();
     ^
main.cpp: In function ‘int main()’:
main.cpp:31:5: error: static assertion failed: C objects are deletable but shouldn't be because destructor is private.
     static_assert(!Deletable<C>::value, "C objects are deletable but shouldn't "
     ^~~~~~~~~~~~~
main.cpp:31:20: error: ‘C::~C()’ is private within this context
     static_assert(!Deletable<C>::value, "C objects are deletable but shouldn't "
                    ^~~~~~~~~~~~
main.cpp:14:5: note: declared private here
     ~C();
     ^
main.cpp: In instantiation of ‘struct Deletable<E>’:
main.cpp:35:32:   required from here
main.cpp:11:76: error: ‘static void E::operator delete(void*)’ is private within this context
 struct Deletable<T, void_t<T, decltype(delete declval<T>())> > : true_type { };
                                                                            ^
main.cpp:27:10: note: declared private here
     void operator delete(void*);
          ^~~~~~~~
main.cpp:35:5: error: static assertion failed: E objects are deletable but shouldn't be because deallocation function is inaccessible.
     static_assert(!Deletable<E>::value, "E objects are deletable but shouldn't "
     ^~~~~~~~~~~~~
main.cpp:35:20: error: ‘static void E::operator delete(void*)’ is private within this context
     static_assert(!Deletable<E>::value, "E objects are deletable but shouldn't "
                    ^~~~~~~~~~~~
main.cpp:27:10: note: declared private here
     void operator delete(void*);
          ^~~~~~~~
...