Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is std::is_same<t,t>::value always true?

I've inherited some code that looks like this:

///
/// A specializable function for converting a user-defined object to a string value
///
template <typename value_type>
std::string to_string(const value_type &value)
{
    static_assert(!std::is_same<value_type, value_type>::value, "Unspecialized usage of to_string not supported");
    return "";
}

///
/// A specializable function for converting a user-defined object from a string to a value
///
template <typename return_type>
return_type from_string(const std::string &source)
{
    static_assert(!std::is_same<return_type, return_type>::value, "Unspecialized usage of from_string not supported");
}

!std::is_same<value_type, value_type>::value seems overly verbose.

Should I change these statements to static_assert(false,"...")?

I'm not sure if it was expressed this way to handle some kind of edge case, or if false is indeed equivalent.

Is std::is_same<t,t>::value always true?

like image 202
Trevor Hickey Avatar asked Jun 30 '17 13:06

Trevor Hickey


1 Answers

The code you posted is ill formed with no diagnostic required.

Replacing it with static_assert(false, ...) makes the compiler notice the fact your code is ill-formed. The code was ill-formed before, the compiler just didn't notice it.

I have two fixes to your problem. One is a hack, but legal. The other is much cleaner, but requires you to write more code.

The first section of this answer is why your code is ill-formed. The next two are solutions.

Why is the code ill-formed?

template <typename value_type>
std::string to_string(const value_type &value)
{
  static_assert(!std::is_same<value_type, value_type>::value, "Unspecialized usage of to_string not supported");
  return "";
}

The primary template of to_string cannot be instantiated with any type. The C++ standard demands that all templates, including primary templates, must have a valid instantiation (which in standardese is called a valid specialization). (There are other requirements, like at least one such instantiation must have an non-empty pack if there are packs involved, etc).

You may complain that "it compiled and worked", but that is no diagnostic required means. The C++ standard places zero constraints on what the compiler does when it runs into a "ill-formed no diagnostic required" case. It can fail to detect it and blithely compile that "works". It can assume it is impossible, and generate malformed code if it does happen. It can attempt to detect it, fail, and do either of the above. It can attempt to detect it, succeed, and generate an error message. It can detect it, succeed, and generate code that emails thumbnails of every image you looked at in your browser over the last year to all of your contacts.

It is ill-formed, and no diagnostic is required.

I would avoid such code myself.

Now, one might argue that someone could somewhere specialize is_same<T,T> to return false, but that would also make your program ill formed as an illegal specialization of a template from std that violates the requirements on the template as written in the standard.

Replacing !std::is_same<value_type, value_type>::value with false will simply permit your compiler to realize your code is ill formed, and generate an error message. This is, in a sense, a good thing, as ill formed code can break in arbitrary ways in the future.

Hack way to fix it

The stupid way to fix this is to create

template<class T, class U>
struct my_is_same:std::is_same<T,U> {};

which admits the possibility of the specialization loophole. This is still code smell.

Right way to fix it

The right way to write both of these requires a bit of work.

First, to_string and from_string based off tag dispatching instead of template specialization:

namespace utility {
  template<class T>struct tag_t {};
  template <typename value_type>
  std::string to_string(tag_t<value_type>, const value_type &value) = delete;
  template <typename value_type>
  std::string to_string(const value_type &value) {
    return to_string(tag_t<value_type>{}, value);
  }

  template <typename return_type>
  return_type from_string(tag_t<return_type>, const std::string &source) = delete;
  template <typename return_type>
  return_type from_string(const std::string &source) {
    return from_string(tag_t<return_type>{}, source);
  }
}

The goal is that the end user simply does a utility::from_string<Bob>(b) or utility::to_string(bob) and it works.

The base ones bounce to tag-dispatches. To customize, you overload the tag-dispatch versions.

To implement the to/from string, in the namespace of Bob write these two functions:

Bob from_string( utility::tag_t<Bob>, const std::string& source );
std::string to_string( utility::tag_t<Bob>, const Bob& source );

notice they are not templates or specializations of templates.

To handle types in std or built-in types, simply define similar overloads in namespace utility.

Now, ADL and tag dispatching take you to the correct to/from string function. No need to change namespaces to define to/from string.

If you call to_string or from_string without a valid tag_t overload, you end up calling the =delete and getting an "overload not found" error.

Test code:

struct Bob {
    friend std::string to_string( utility::tag_t<Bob>, Bob const& ) { return "bob"; }
    friend Bob from_string( utility::tag_t<Bob>, std::string const&s ) { if (s=="bob") return {}; exit(-1); }
};

int main() {
    Bob b = utility::from_string<Bob>("bob");
    std::cout << "Bob is " << utility::to_string(b) << "\n";
    b = utility::from_string<Bob>( utility::to_string(b) );
    std::cout << "Bob is " << utility::to_string(b) << std::endl;
    Bob b2 = utility::from_string<Bob>("not bob");
    std::cout << "This line never runs\n";
    (void)b2;
}

Live example.

(Use of friend is not required, the function just has to be in the same namespace as Bob or within namespace utility).

like image 77
Yakk - Adam Nevraumont Avatar answered Nov 04 '22 11:11

Yakk - Adam Nevraumont