JiaHao Xu
JiaHao Xu

Reputation: 2748

write a concept check library, but fail on all fundemental arithmetic types

Things went wrong when I was testing whether fundemental arithmetic types like int, and raw pointers like int*, can be pre-incrementabl/post-incrementable.

//has_operator_overload_functions.hpp
namespace concept_check {
template <class T>
T create_T();

struct disambiguation_t2 {};
struct disambiguation_t1 {
    constexpr operator disambiguation_t2 () const noexcept;
};
}

# define create_has_op_overload_fn_test(fn_name, ret_t_of_op_to_test) \
template <class T>                                          \
auto has_ ## fn_name (disambiguation_t1) -> ret_t_of_op_to_test;     \
template <class T>                                          \
void has_ ## fn_name (disambiguation_t2);                    \
template <class T>                                          \
inline constexpr const bool has_ ## fn_name ## _v = !is_same_v<decltype(has_ ## fn_name <T>(disambiguation_t1{})), void>;

# define create_check_op_overload_fn_return_type_test(fn_name, ret_t_of_op_to_test, result_t) \
create_has_op_overload_fn_test(fn_name, ret_t_of_op_to_test) \                              
_create_check_op_overload_fn_return_type_test(fn_name, ret_t_of_op_to_test, result_t)

# define _create_check_op_overload_fn_return_type_test(fn_name, ret_t_of_op_to_test, result_t) \
template <class T>          \                                                         
constexpr const bool is_ ## fn_name () noexcept {      \                           

    if constexpr(has_ ## fn_name ## _v<T>)                  \                                 
        return is_convertible_v< ret_t_of_op_to_test , result_t >; \
    else             \                                
        return 0;                                \
}                                                  \
template <class T>                                      \
inline constexpr const bool is_ ## fn_name ## _v = is_ ## fn_name <T>();

namespace concept_check {
create_check_op_overload_fn_return_type_test(pre_incrementable, 
decltype(++create_T<T>()), T&)
create_check_op_overload_fn_return_type_test(post_incrementable, 
decltype(create_T<T>()++), T)
}//namespace concept_check

//test.hpp
#include <iostream>
#include "has_operator_overload_functions.hpp"
using namespace concept_check;
struct A {
    A& operator ++ ();
    A operator ++ (int);
};
struct B {
    B& operator ++ ();
};
struct C {
    C operator ++ (int);
};
struct E {};
int main() {
    std::cout << std::boolalpha

              << "incrementable\n\n"

              << "int\n"
              << is_pre_incrementable_v<int> << std::endl
              << is_post_incrementable_v<int> << std::endl

              << "\nchar\n"
              << is_pre_incrementable_v<char> << std::endl
              << is_post_incrementable_v<char> << std::endl

              << "\nfloat\n"
              << is_pre_incrementable_v<float> << std::endl
              << is_post_incrementable_v<float> << std::endl

              << "\ndouble\n"
              << is_pre_incrementable_v<double> << std::endl
              << is_post_incrementable_v<double> << std::endl

              << "\nchar*\n"
              << is_pre_incrementable_v<char*> << std::endl
              << is_post_incrementable_v<char*> << std::endl

              << "\nvoid*\n"
              << is_pre_incrementable_v<void*> << std::endl
              << is_post_incrementable_v<void*> << std::endl

              << "\nA\n"
              << is_pre_incrementable_v<A> << std::endl
              << is_post_incrementable_v<A> << std::endl

              << "\nB\n"
              << is_pre_incrementable_v<B> << std::endl
              << is_post_incrementable_v<B> << std::endl

              << "\nC\n"
              << is_pre_incrementable_v<C> << std::endl
              << is_post_incrementable_v<C> << std::endl

              << "\nE\n"
              << is_pre_incrementable_v<E> << std::endl
              << is_post_incrementable_v<E> << std::endl

              << "\nbool\n"
              << is_pre_incrementable_v<bool> << std::endl
              << is_post_incrementable_v<bool> << std::endl
}

I compiled it using

clang version 5.0.1-svn324012-1~exp1 (branches/release_50)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/6
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/6.3.0
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/6
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/6.3.0
Selected GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/6.3.0
Candidate multilib: .;@m64
Selected multilib: .;@m64

, with command line args -std=c++17 -I .. and ran the binary file I got, and it gave out the following output:

incrementable

int
false
false

char
false
false

float
false
false

double
false
false

char*
false
false

void*
false
false

A
true
true

B
true
false

C
false
true

E
false
false

bool
false
false

As you can see, all the fundemental arithmetic types and raw pointers didn't pass the pre-icrementable/post-incrementable test.

Can any explain why this happened and how to fix this?

Upvotes: 1

Views: 96

Answers (1)

Luc Danton
Luc Danton

Reputation: 35449

create_check_op_overload_fn_return_type_test(pre_incrementable, decltype(++create_T<T>()), T&)

You are using decltype(++create_T<T>()) as an effective constraint on your is_pre_incrementable_v check. For a type such as A, this is equivalent to testing e.g. ++A {} which is valid, as reflected in your results.

For a type such as int however this is equivalent to testing ++0, which is not a valid expression. (And so on for the other non-class types.)

There isn't an obvious fix. You could decide to constrain on e.g. ++create_T<T&>() instead (it’s also a good time to mention std::declval). But pay attention that this has the following consequence:

template<typename Arg>
auto under_constrained(Arg const& arg)
-> std::enable_if_t<is_pre_incrementable_v<Arg>>
{
    ++arg;
}

template<typename Arg>
auto over_constrained(Arg& arg)
-> std::enable_if_t<is_pre_incrementable_v<Arg>, std::decay_t<Arg>>
{
    auto copy = arg;
    ++copy;
    return copy;
}
  • the first example is under-constrained because it effectively tests for ++mutable_arg given an imaginary Arg mutable_arg; variable, but the body operates on an immutable variable (which of course will not be incrementable in most circumstances)
  • the second example is over-constrained because e.g. if called on int const immutable_variable = 0; as an argument it effectively tests for ++immutable_arg, but the body operates on an int variable

Now, these example could be tweaked so that they are instead constrained on e.g. is_pre_incrementable_v<Arg const&> and is_pre_incrementable_v<std::decay_t<Arg>&> respectively. But that requires care on the concept user’s part. That’s why the fix is not necessarily the go-to solution.

It all comes down to the meaning of template parameters for a given concept.

Upvotes: 2

Related Questions