I found this great article on how to implement Concepts in C 14. In one section about how to make a compiles
checker he gives the following:
template <typename ... Ts>
using void_t = void;
template <typename T, template <typename> class Expression, typename AlwaysVoid = void_t<>>
struct compiles : std::false_type {};
template <typename T, template <typename> class Expression>
struct compiles<T, Expression, void_t<Expression<T>>> : std::true_type {};
The above code will check if an expression compiles, but has no checks on the return type. Later on he mentioned that a compiles_convertible_type
and compiles_same_type
checker can be created by wrapping the compiles
trait but doesn't give an example of how to do that, stating that it is simple. However, I am somewhat new to SFINAE so I'm not sure exactly what to do here.
template <typename T, typename Result, template <typename> class Expression>
struct compiles_convertible_type : /* some invocation of compiles<> trait here */
template <typename T, typename Result, template <typename> class Expression>
struct compiles_same_type : /* some invocation of compiles<> trait here */
For reference, this is what I have tried, but it returns true for everything. I think because the is_same
and is_convertible
expressions compile.
template <typename T, typename Result, template <typename> class Expression>
struct compiles_convertible_type :
compiles<T, Expression, void_t<std::is_convertible<Result, std::result_of<Expression<T>>>>> {};
template <typename T, typename Result, template <typename> class Expression>
struct compiles_same_type :
compiles<T, Expression, void_t<std::is_same<Result, std::result_of<Expression<T>>>>> {};
namespace memory {
struct memory_block{};
}
struct MyAllocator {
memory::memory_block allocate_block(){return {};};
void deallocate_block(memory::memory_block){};
std::size_t next_block_size() const {return 0;};
};
struct MyBadAllocator {
memory::memory_block allocate_block(){return {};};
void deallocate_block(memory::memory_block){};
void next_block_size() const {};
};
template <typename T>
struct BlockAllocator_impl
{
template <class Allocator>
using allocate_block = decltype(std::declval<Allocator>().allocate_block());
template <class Allocator>
using deallocate_block = decltype(std::declval<Allocator>().deallocate_block(std::declval<memory::memory_block>()));
template <class Allocator>
using next_block_size = decltype(std::declval<const Allocator>().next_block_size());
using result = std::conjunction<
compiles_convertible_type<T, memory::memory_block, allocate_block>,
compiles<T, deallocate_block>,
compiles_same_type<T, std::size_t, next_block_size>
>;
using has_allocate_block = compiles_convertible_type<T, memory::memory_block, allocate_block>;
using has_deallocate_block = compiles<T, deallocate_block>;
using has_next_block_size = compiles_same_type<T, std::size_t, next_block_size>;
};
template <typename T>
using BlockAllocator = typename BlockAllocator_impl<T>::result;
template <typename T>
using BlockAllocatorAllocate = typename BlockAllocator_impl<T>::has_allocate_block;
template <typename T>
using BlockAllocatorDeallocate = typename BlockAllocator_impl<T>::has_deallocate_block;
template <typename T>
using BlockAllocatorNextBlockSize = typename BlockAllocator_impl<T>::has_next_block_size;
#include <fmt/core.h>
int main()
{
fmt::print("MyBadAllocator\n");
fmt::print("has allocate: {}\n", BlockAllocatorAllocate<MyBadAllocator>::value);
fmt::print("has deallocate: {}\n", BlockAllocatorDeallocate<MyBadAllocator>::value);
fmt::print("has next block size: {}\n", BlockAllocatorNextBlockSize<MyBadAllocator>::value);
fmt::print("Is BlockAllocator: {}\n", BlockAllocator<MyBadAllocator>::value);
fmt::print("MyAllocator\n");
fmt::print("has allocate: {}\n", BlockAllocatorAllocate<MyAllocator>::value);
fmt::print("has deallocate: {}\n", BlockAllocatorDeallocate<MyAllocator>::value);
fmt::print("has next block size: {}\n", BlockAllocatorNextBlockSize<MyAllocator>::value);
fmt::print("Is BlockAllocator: {}\n", BlockAllocator<MyAllocator>::value);
}
output:
MyBadAllocator
has allocate: true
has deallocate: true
has next block size: true // expect false
Is BlockAllocator: true // expect false
MyAllocator
has allocate: true
has deallocate: true
has next block size: true
Is BlockAllocator: true
CodePudding user response:
The following implementations give exactly the expected output (and probably those are close to what the author of the article used behind the scenes):
template <typename T, typename Result, template <typename> class Expression, typename AlwaysVoid = void_t<>>
struct compiles_same_type : std::false_type {};
template <typename T, typename Result, template <typename> class Expression>
struct compiles_same_type<T, Result, Expression, void_t<Expression<T>>> : std::is_same<Result, Expression<T>> {};
template <typename T, typename Result, template <typename> class Expression, typename AlwaysVoid = void_t<>>
struct compiles_convertible_type : std::false_type {};
template <typename T, typename Result, template <typename> class Expression>
struct compiles_convertible_type<T, Result, Expression, void_t<Expression<T>>> : std::is_convertible<Result, Expression<T>> {};
The trick is to replace the inherited std::true_type
on the template specialization selected by SFINAE when the expression is valid with other struct/class that does the desired type check (aka a type trait in C 's standard library terminology). Of course you should modify the template parameter list of the initial compiles
type with the extra parameters needed by the inherited type trait (in those cases only Result
is needed as both std::is_same
and std::is_convertible
have only two template type parameters).
In addition, the std::result_of
is both unnecessary and wrong. std::is_same
will check if the value of Result
type parameter is something staring with std::result_of
rather than the actual type of the expression. You probably would want something like std::result_of<...>::type
, but that will be an invalid as Expression<T>
is not a callable type in any usage from the BlockAllocator example. You should keep in mind that the actual value of Expression
is already the resulted type and not the expression itself:
template <class Allocator>
using allocate_block = decltype(std::declval<Allocator>().allocate_block());
you should interpret that as "the resulted type of the expression a.allocate_block()
where a
has Allocator
type.
If you want to see more information about std::result_of
then cppreference is a good place to start: https://en.cppreference.com/w/cpp/types/result_of