Home > database >  How to Implement Concepts in C 17
How to Implement Concepts in C 17

Time:11-02

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

  • Related