21

I am trying to write a template is_c_str to test if a type is a c-style string. I need this as an attempt to write a to_string function, as shown in my other question here: Template specialization for iterators of STL containers?.

I need to tell apart c_str and other types of pointers and iterators, so that I can represent the first at the face value, and render pointers/iterators as an opaque "itor" or "ptr". The code is as follows:

#include <iostream>
template<class T>
struct is_c_str
  : std::integral_constant<
  bool,
  !std::is_same<char *, typename std::remove_reference<typename std::remove_cv<T>::type>::type>::value
> {};

int main() {
  auto sz = "Hello";  //Or: const char * sz = "Hello";
  int i;
  double d;
  std::cout << is_c_str<decltype(sz)>::value << ", "
        << is_c_str<decltype(i)>::value << ", "
        << is_c_str<decltype(d)>::value << std::endl;
}

However, is_c_str captures not only const char *, but also int and double. The above code outputs:

1, 1, 1

(as of gcc-4.8.1).

My question is how to fix is_c_str to properly capture c-style strings?

Community
  • 1
  • 1
thor
  • 21,418
  • 31
  • 87
  • 173
  • 2
    Is `remove_reference>` not the same as `char *`? Why yes, that's true. – chris Jul 20 '14 at 21:41
  • 2
    Why do you need this? Maybe there is an easier way to solve your original problem. – Neil Kirk Jul 20 '14 at 21:44
  • 9
    Be aware that `char*` is just a pointer to `char`. It's not a C-style string unless it points to a null-terminated array. – user3553031 Jul 20 '14 at 21:45
  • @NeilKirk, Please see the P.S. – thor Jul 20 '14 at 21:47
  • Is the type of `sz` really what you think it is? – Kerrek SB Jul 20 '14 at 21:48
  • @KerrekSB, Considering they said it captures `const char *`, maybe. – chris Jul 20 '14 at 21:50
  • @KerrekSB I tried const char * sz = "Hello2"; as well, same result. – thor Jul 20 '14 at 21:50
  • 6
    Note that `wchar_t`, `char16_t` and `char32_t` can and are used for C-style strings as well. – dyp Jul 20 '14 at 22:02
  • To expand on dyp's comment: C-style strings are arrays of the member type, where the last one and only that last one is a 0-terminator. Pointers to them are pointers to C-style strings, but are not C-style strings themselves. – Deduplicator Jul 20 '14 at 22:31
  • 3
    For your particular use case, I'd just add an overload of `template to_string(T * str);` and SFINAE it out unless `std::decay_t` is one of `char`, `wchar_t`, `char16_t` and `char32_t`. – T.C. Jul 20 '14 at 22:43
  • This would also catch `vector vec(.....);` with `begin(vec)` if the compiler used bare pointers as vector iterators (which is permitted). Maybe even consider rejecting `char *` entirely since there is no way you can validate if it points into a string or not; put the onus on the caller to supply the data in a safe manner. – M.M Jul 21 '14 at 01:39

5 Answers5

22

You want to check if the type is the same as a char *, but you're negating the result of std::is_same, which is clearly not going to produce the correct result. So let's remove that.

template<class T>
struct is_c_str
  : std::integral_constant<
      bool,
      std::is_same<char *, typename std::remove_reference<typename std::remove_cv<T>::type>::type>::value
> {};

However, this is now going to result in the output 0, 0, 0. The problem now is that remove_cv removes top-level cv-qualifiers, but the const in char const * is not top-level.


If you want to match both char * and char const * the easiest solution is:

template<class T>
struct is_c_str
  : std::integral_constant<
      bool,
      std::is_same<char *, typename std::remove_reference<typename std::remove_cv<T>::type>::type>::value ||
      std::is_same<char const *, typename std::remove_reference<typename std::remove_cv<T>::type>::type>::value
> {};

The above version will still not match char[]. If you want to match those too, and reduce the verbosity of combining std::remove_reference and std::remove_cv, use std::decay instead.

template<class T>
struct is_c_str
  : std::integral_constant<
      bool,
      std::is_same<char const *, typename std::decay<T>::type>::value ||
      std::is_same<char *, typename std::decay<T>::type>::value
> {};
Praetorian
  • 106,671
  • 19
  • 240
  • 328
  • You can reduce the verbosity even more by using `std::decay_t`. – Felix Glas Jul 20 '14 at 22:05
  • 1
    @Snps The question is not tagged [tag:c++1y], otherwise I agree with you. – Praetorian Jul 20 '14 at 22:08
  • You can trivially disprove an argument being a C-Style string, if it has the wrong type (i.e. not type array of string-member-type). You cannot do so if it has type array of string-member-type of unknown length. Also, for pointers to a string the problem is magnified: You need also the element-number. – Deduplicator Jul 20 '14 at 22:37
9

I tried this and it seems to work:

#include <iostream>

template<class T>
struct is_c_str : std::integral_constant<bool, false> {};

template<>
struct is_c_str<char*> : std::integral_constant<bool, true> {};

template<>
struct is_c_str<const char*> : std::integral_constant<bool, true> {};

int main() {
  auto sz = "Hello";
  int i;
  double d;
  std::cout << is_c_str<decltype(sz)>::value << ", "
        << is_c_str<decltype(i)>::value << ", "
        << is_c_str<decltype(d)>::value << std::endl;
}

Obviously enumerating every case is not as elegant as putting the general predicate in std:integral_constant, but on the other hand that predicate is alien tongue for idiots like me, while the "brute force" template specialization is somewhat more comprehensible, and viable in this case as there are few specializations.

gpeche
  • 21,974
  • 5
  • 38
  • 51
  • 1
    There are actually more that this and other answers don't address -- such as `wchar_t`, `char`, `unsigned char`, `char16_t`, and `char32_t`. – Rapptz Jul 21 '14 at 06:42
  • @Rapptz I looked into it; there's no `std::is_character_type` so you just have to hard-code such a list. – Potatoswatter Jul 21 '14 at 07:40
  • @Potatoswatter Yeah, it's quite a shame honestly. A trait like that is pretty easy to define yourself but still sad that the standard library doesn't provide one. – Rapptz Jul 21 '14 at 07:42
  • Also works with using std::true_type and std::false_type. – Dominik Grabiec Sep 23 '16 at 16:59
8

The type of sz is char const*, but std::remove_cv<> only removes top-level const, so you can't get char* through its application. Instead, you can just check for a char const* after completely decomposing the type with std::decay<>:

namespace detail
{
    template<class T>
    struct is_c_str : std::is_same<char const*, T> {};
}

template<class T>
struct is_c_str : detail::is_c_str<typename std::decay<T>::type> {};

int main() {
  auto sz = "Hello";
  int i;
  double d;
  std::cout << is_c_str<decltype(sz)>::value << ", "
            << is_c_str<decltype(i)>::value << ", "
            << is_c_str<decltype(d)>::value << std::endl;
}

You were also falsely negating the condition. I fixed that as well.

Live Example

David G
  • 94,763
  • 41
  • 167
  • 253
4

There are a few solutions already, but since the simplest solution is really simple, I'll jot it down here.

template< typename, typename = void >
struct is_c_str
    : std::false_type {};

template< typename t >
struct is_c_str< t *,
    typename std::enable_if< std::is_same<
        typename std::decay< t >::type,
        char
    >::value >::type
>
    : std::true_type {};

The tricky part, of course, is analyzing what's inside the pointer type instead of the pointer type itself.

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
2

There are couple of problems.

  1. The line

    !std::is_same<char *, typename std::remove_reference<typename std::remove_cv<T>::type>::type>::value
    

    needs to be

    std::is_same<char *, typename std::remove_reference<typename std::remove_cv<T>::type>::type>::value
    
  2. Your logic of using typename std::remove_reference<typename std::remove_cv<T>::type>::type>::value is flawed. It does not convert char const* to char*. It can convert char* const to char*.

What you need is:

template<class T>
struct is_c_str
  : std::integral_constant<
  bool,
  std::is_same<char *, typename std::remove_reference<typename std::remove_cv<T>::type>::type>::value ||
  std::is_same<char const*, typename std::remove_reference<typename std::remove_cv<T>::type>::type>::value
> {};
R Sahu
  • 204,454
  • 14
  • 159
  • 270