c++ Type confusion stopper

C++ is hard. But, you are advancing.  Things are becoming clearer and clearer.

By now, you feel confident enough to tackle head-on, even the c++ meta-programming, perhaps? Compile-time shenanigans and the rest?

(for the clever ones, Wandbox is here)

One tool, in particular, seems very powerful: std::is_same<T1, T2> . Using this tool, one can achieve compile-time “magic” based on equality of two types.  For example.

// std::is_same typical use case
template<typename T>
 auto function ( T t_ ) 
{
// the constexpr if statement
// https://en.cppreference.com/w/cpp/language/if
   if constexpr ( std::is_same_v<char, T> ) {
     // do something with chars only
     } else {
     // do something with any type
     // that is not char
   }
}

if T is found at compile time to be of type char, branch into one algorithm, if not branch to another one. Very simple and powerful indeed. What could possibly go wrong?

And you are ready for a serious C++. But.  Consider this canonical function template:

template< typename T> 
void function( T t_ ) {
 // what is the type of T in here?
}

Somehow,  T is almost never a type you expect it to be. And now you have to admit, probably you have not read the fundamentals of standard C++ before starting this journey on “the path peppered with the shards of glass”?  My  preferred answer to that simple question “what is the type of T” is:

One needs to know what exactly the type T can be

Side note: Speaking of types we immediately bump into SFINAE. SFINAE is one thing almost every C++ beginner puts under the carpet as long as possible. Until it has to be understood and used. This tool will help you too in that noble endeavor.

The core of success is your complete understanding of the classification of types in standard C++. Here is a quick set of C++ type names divided into two base categories:

  1. Compound types
    Array, function, object pointer, function pointer, member object pointer, member function pointer, reference, class, union, or enumeration, including any cv-qualified variants.
  2. Fundamental types
    Arithmetic type (integral type or a floating-point type), void, or nullptr_t. Where the integral types are:  bool, char, char16_t, char32_t, wchar_t, short, int, long, long long, or any implementation-defined extended integer types, including any signed, unsigned, and cv-qualified variants

Hint: that “cv-qualified variants” is where the trouble starts.

Here is the situation recap. You have bravely dived into the standard C++ types taxonomy, and you have developed the habit to use standard and useful <type_traits> to fight it out with the types. And it seems so simple and logical when looking into examples online. But not always so, when you try it yourself.

The Solution

To help myself out ( and for painless use of other C++ std::  templated goodies), I have developed a few useful template aliases, for taming this uncertain situation around the “What is the type of  T” proverbial question.

Here I will present a tool that will reduce ‘anything’ to the base type.   Please look again, into that classification above. Seriously. Back? Ok. So,  here is a tool that will reduce any type T to its base fundamental type. Its most canonical usage:

template < typename T>
void function ( T t_ ) {
   // whatever  is T we can 
   // use it's base type
   // with the help of
   // dbj::to_base_t<>
 using base_type = dbj::to_base_t<T> ; 
}

Big note.

“Base type” is not a fundamental type.

The base type is a fundamental unqualified type which is a base of a compound type T. And if T is not compound type, in that case, this tool gives the unqualified form of T.

Much like std::decay<T> does. Actually this tools is something like std decay on steroids. To use the blogger’s cliche.

The Usage

To clarify this completely, let me show you some hard testing first. Here we use several funny but legal C++ compound types, invented for testing only.

// these are all compile time tests
// char is base type, deduced from six levels of pointers to char
// is_same_v will have no problems to recognize that base type as a char
// with the help of dbj::to_base_t
	static_assert(dbj::is_same_v<char ,dbj::to_base_t<char ******>>, "Failed!" );

// std::string is base type for array of pointers to std::string
	static_assert(dbj::is_same_v<std::string ,dbj::to_base_t<std::string * (&)[]>>, "Failed!" );

// can do function types too, of course
// void () function base type is void () function
	static_assert(dbj::is_same_v<void (), dbj::to_base_t<void ()>>, "Failed!") ;
    
    struct X { char data{}; char method () const { return {};} };
    // base type is X for an reference to array of X
	static_assert(dbj::is_same_v<X, dbj::to_base_t<X (&) []>>, "Failed!" );
    
    // pointer to method on X 
    using method_t = char (X::*)() ;
    // base type is that metod, deduced from an ref to the array of method pointers
	static_assert(dbj::is_same_v<method_t, dbj::to_base_t< method_t (&) []>>, "Failed!" );

All compile-time tets. As you can see  dbj::to_base_t<T>  really works. Whatever (compound) type you give to it, the result will be reduced to its unqualified base type.

Synopsis

This tool will also work for double, triple, etc pointers. And here is the tool itself:

// (c) 2019/2020 by dbj@dbj.org 
// CC BY SA 4.0
//
// https://wandbox.org/permlink/h8BE47pvLlBMy7wy
// clang++ prog.cc -std=c++14
// OK for C++14, C++17, C++2a
//
#include <iostream>
#include <string>

namespace dbj 
{
    #if __cplusplus < 201703L
    // for C++14
    // inline variables are a C++17 extension too
    template< class T, class U >
      static constexpr bool is_same_v = std::is_same<T, U>::value;
    #endif
	
    template <typename T> struct remove_all_ptr { typedef T type;	};

	template <typename T> struct remove_all_ptr<T*>	{
		using type = typename remove_all_ptr<std::remove_cv_t<T>>::type;
	};
    
    template <typename T>
    using remove_all_ptr_t = typename remove_all_ptr<T>::type ;
    
template< class T >
struct remove_cvref {
	typedef std::remove_cv_t<std::remove_reference_t<T>> type;
};

template< class T >
using remove_cvref_t = typename remove_cvref<T>::type;

template <class T>
using to_base_t =
  remove_all_ptr_t< std::remove_all_extents_t< remove_cvref_t < T > > >;
    
} // dbj

Your problem function from above will now be changed to use dbj::to_base_t .

// std::is_same use case
template<typename T>
auto function ( T t_ ) 
{
// std::decay_t will not be very useful here
// **whatever** is T we need to know if 'char'
// is the base type of T
if constexpr ( 
     std::is_same_v<
       char, 
// reduce T to it's base type
         dbj::to_base_t<T>
      > 
  ) {
// do something special with chars
} else {
// do something with the rest
}
}

And remember the return type form that function can be only one. One C++ function can return only one type.

Feel free to copy-paste and use it. Just please leave in the copyright statement.

Obligatory Gist is here. That works for C++14, 17, and 20.

 

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.