3

I am trying to write code that will be able to distinguish between character types (char, wchar_t, etc.), string types (std::string, std::wstring, etc.), and numeric types, so I can enclose characters in single-quotes and strings in double-quotes. The idea is to treat values differently depending on how they are output. Characters and strings are fundamentally different from numeric values, because they display according to an encoding representation of their contents (i.e., ASCII, Unicode, UTF, etc.), rather than as numeric values.

(Note: this code is extracted from a much larger and more complicated program)

Here's my code, compiled with

g++ -std=c++14 testchar.cpp -o testchar

which works under Linux Mint 18.3 (Sylvia) compiled with g++ v5.4.0

#include <iostream>
#include <locale>
#include <codecvt>
#include <string>
#include <type_traits>

using std::cout;
using std::is_same;
using std::string;
using std::u16string;
using std::u32string;
using std::wstring;

#define is_string_type(T)  ( is_same<T,string>::value    || is_same<T,wstring>::value   || \
                             is_same<T,u16string>::value || is_same<T,u32string>::value )
#define is_char_type(T)    ( is_same<T,char>::value      || is_same<T,wchar_t>::value   || \
                             is_same<T,char16_t>::value  || is_same<T,char32_t>::value  )
#define is_numeric_type(T) ( !is_char_type(T) && std::is_arithmetic<T>::value )

template <typename T>
typename std::enable_if<is_string_type(T),void>::type
output_value( const string& name, const T& strval ) {
    cout << "String " << name << " is \"" << strval << "\";\n";
}
template <typename T>
typename std::enable_if<is_char_type(T),void>::type
output_value( const string& name, const T& chrval ) {
    cout << "Character " << name << " is '" << chrval << "';\n";
}
template <typename T>
typename std::enable_if<is_numeric_type(T),void>::type
output_value( const string& name, const T& val ) {
    cout << "Numeric " << name << " is " << val << ";\n";
}

int main(void)
{
    string    name;
    short     sval = 4321;
    int       ival = 123;
    long      lval = 1234567890L;
    char      cval = 'W';
    string    Sval = "string";

    name = "sval";
    output_value( name, sval );
    name = "ival";
    output_value( name, ival );
    name = "lval";
    output_value( name, lval );
    name = "cval";
    output_value( name, cval );
    name = "strval";
    output_value( name, Sval );

    return 0;
}

But my macros, 'is_char_type' and 'is_string_type' are ugly and not very robust. And they are macros... yuck! I did try using std::is_base_of<std::basic_string,T>::value for `is_string_type', but the compiler threw an error:

testchar.cpp:17:65: error: type/value mismatch at argument 1 in template parameter list for ‘template<class, class> struct std::is_base_of’

If anyone knows a better way to do this please let me know! I'm kind of surprised these (is_character_type and is_string_type) don't already exist in type_traits... or perhaps they do, but are cleverly disguised?

SGeorgiades
  • 1,771
  • 1
  • 11
  • 11
  • The set of character types is finite and fixed (not withstanding new standard versions. C++20 will add yet another one: `char8_t`). The set of *string* types is not. Lots of people have string types, and indeed even the standard library has `basic_string_view` in addition to `basic_string`. So really, it seems to me like you're asking the wrong question. – Nicol Bolas Jul 02 '18 at 03:26
  • 1
    The most straightforward way to avoid the macros is to define traits like the `is_same` one you use. – chris Jul 02 '18 at 03:28

2 Answers2

2
template<class T>struct tag_t{};
template<class T>constexpr tag_t<T> tag{};
namespace detect_string {
  template<class T, class...Ts>
  constexpr bool is_stringlike(tag_t<T>, Ts&&...){ return false; }
  template<class T, class A>
  constexpr bool is_stringlike( tag_t<std::basic_string<T,A>> ){ return true; }
  template<class T>
  constexpr bool detect=is_stringlike(tag<T>); // enable ADL extension
}
namespace detect_character {
  template<class T, class...Ts>
  constexpr bool is_charlike(tag_t<T>, Ts&&...){ return false; }
  constexpr bool is_charlike( tag_t<char> ){ return true; }
  constexpr bool is_charlike( tag_t<wchar_t> ){ return true; }
  // ETC
  template<class T>
  constexpr bool detect=is_charlike(tag<T>); // enable ADL extension
}

now detect_character::detect<char> is true, as is detect_string::detect<std::wstring>.

If you want only strings of charlikes to be strings, add it to the is_stringlike overload.

You can extend either of these by defining within the namespace of a type X a is_stringlike(tag_t<X>) overload, and it will be automatically found. Or doing so in detect_stringlike. You may not add overloads to std in this way, so do them in the detect_stringlike namespace.

There are other solutions, but this is the only one that avoids the fragility of a single central list.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Thank you. It will take me a bit of time to absorb this, but I think I get the idea. – SGeorgiades Jul 02 '18 at 04:42
  • After looking at this for a bit, it seems that `detect_string` does exactly what I wanted. Thanks. But for `detect_character`, I was hoping it would not be necessary to itemize every character type explicitly but to have a trait-class -- similar to `is_integral` -- that would identify types that are handled differently by -- for example -- iostream, which outputs a character represented by the value rather than the value itself. To just itemize every known character-type is ugly, and not very extensible. – SGeorgiades Jul 02 '18 at 05:20
  • As mentioned above, c++20 will introduce a new `char8_t`. It would be nice to have a trait to check so this would work even in the face of future language enhancements. In fact, I am surprised that such a trait doesn't already exist. If `typeid(T).name` could be relied on to have consistent results, I would just check if the type `is_integral` and its name contains the string `char`. I really hate brute-force solutions, and it is a bad design practice to just list all the possibilities individually. In OOP, an object should be able to tell you about important characteristics for itself. – SGeorgiades Jul 02 '18 at 05:23
  • @SGeorgiades To be clear, 17 is not the value of 0x00000011. Neither is 0x00000011 the value of 0b010001. Nor is it the 18th letter of the alphabet, the 8th letter of the alphabet, or the unicode code point corresponding to 17 (which I believe is unprintable). They are all representations. And iostream doesn't advertise in-language which types it formats differently than others. I'm unaware of a trait that approximates what you want; so I wrote one. – Yakk - Adam Nevraumont Jul 02 '18 at 10:58
  • Yes, @yakk, I understand your point. But I think you can see there is a fundamental difference between choose a radix in which to display a numeric value, versus a character-set encoding. But what I think I am taking away from this is that the character-trait I am seeking does not exist, so the only way to implement it myself is to brute-force it. I would have hoped for more from a OO language that is so mature. Thank you for your input – SGeorgiades Jul 02 '18 at 16:47
  • 2
    @SGeorgiades No, there is no fundamental difference between choosing a radix or a character set encoding. They are both interpretations of bits, stored in a value. Computers have many operations that treat bits like integers; that doesn't mean they are integers. C++ is not an OO language; C++ supports OO programming, but not everything in C++ is an object. C++ is a functional, OO, procedural, structured, high level assembly, statically and dynamically typed, and generic language. But usually not all at the same time. – Yakk - Adam Nevraumont Jul 02 '18 at 19:40
  • Can [std::char_traits](https://en.cppreference.com/w/cpp/string/char_traits) be used to detect character types? – Jakub Klinkovský Jan 30 '20 at 10:14
  • @jakub last I checked it wasn't specified as SFINAE friendly. It could have changed. If not SFINAE friendly, using it to detect isn't possible. – Yakk - Adam Nevraumont Jan 30 '20 at 12:25
1

Following patterns in the std template library you could write helpers as follows:

// Given `std::` namespace ...
template <bool _Val>
using bool_constant = integral_constant<bool, _Val>;

// EdgeS AfBox `pal::` namespace - platform-abstraction-layer type support
// Char8, UChar8, SChar8, UChar16t, WChar16, UChar16, UChar32
typedef char8_t                         UChar8t;
typedef unsigned char                   UChar8;
typedef signed char                     SChar8;
typedef char                             Char8;
// ..other defn's elided..

// EdgeS AfBox `pal::` namespace - platform-abstraction-layer type support
template<typename T>
constexpr bool is_char_type_v = std::_Is_any_of_v<std::remove_cv_t<T>,
  Char8, UChar8, SChar8, UChar16t, WChar16, UChar16, UChar32>;
template<typename T>
struct is_char_type : std::bool_constant <is_char_type_v<T>> {};

// Use it as follows:
template<typename char_ta,
  typename std::enable_if<pal::is_char_type<char_ta>::value>::type* = nullptr>
void populate_arg(AfBlob&& bArgs, char_ta* z) {
  //..ellided..
}
smallscript
  • 643
  • 7
  • 13