I'm interested in programmatically determining whether a delete-expression is valid for some given object. It seems like one should be able to do this using SFINAE. The code I have come up with works like I expect with Clang 6.0.1, but fails to compile with GCC 7.3.0.
Two questions: 1) is there a better way to achieve the goal, and 2) who's right?
#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;
}
My attempt at answering q.2
tl;dr: I think Clang is right. Skip to the bottom for a few other test cases.
Here's why, using N4140.
The relevant section about the delete-expression when given a class type reads [expr.delete] p.1:
[...] If of class type, the operand [of the delete-expression] is contextually implicitly converted to a pointer to object type. The delete-expression’s result has type
void.
Note that void * is not a "pointer to object". Here we find that the result of the decltype(delete <blah>) is void: it is not a dependent expression. However, I think for SFINAE we're just interested in whether or not it's well-formed.
The conversion is done according to [conv] p.5:
An expression e of class type E appearing [as a delete-expression's operand] is said to be contextually implicitly converted to a specified type T and is well-formed if and only if e can be implicitly converted to a type T that is determined as follows: E is searched for conversion functions whose return type is cv T or reference to cv T such that T is allowed by the context. There shall be exactly one such T.
Since T needs to be a pointer-to-object, we see that class A meets the conversion criteria (and is therefore deletable) by providing a single conversion-to-object-pointer function whereas B, providing two such functions, does not. Thus, trying to delete a B object should result in an ill-formed expression. This is what the static_asserts in the above code are supposed to ensure.
Possible alternative approach: can one test an object for "convertability to some pointer to object type"?
Carrying on, here's the definition of an invalid expression for the purpose of type substitution [type.deduct] p.8:
If a substitution results in an invalid type or expression, type deduction fails. An invalid type or expression is one that would be ill-formed, with a diagnostic required, if written using the substituted arguments. [...] Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure. [ Note: The evaluation of the substituted types and expressions can result in side effects such as the instantiation of class template specializations and/or function template specializations, the generation of implicitly-defined functions, etc. Such side effects are not in the “immediate context” and can result in the program being ill-formed. — end note ]
As far as I can tell, since deleting a B object is ill-formed (with diagnostic required), the decltype(delete <b>) expression should be invalid making SFINAE kick in. Deletable then ends up inheriting from false_type, and we're golden.
With Clang, that seems to be the case. With GCC, here's the result:
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
My guess is that GCC does not consider the contextual implicit conversion as part of the immediate context, so the ambiguity becomes a hard error rather than just a type deduction failure. Given the apparent intent of the "immediate context" as defined in the note above, I think the conversion should be in the immediate context. Therefore, as above, I think Clang is right.
For completeness, other possible reasons an object may not be deletable are:
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;
}
Everything works fine in Clang, and, interestingly, the static_assert() for D passes for GCC, however the C and E cases fail.
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*);
^~~~~~~~