55

I'm compiling the following program using Microsoft Visual C++, as a C++20 program:

#include <iostream>
#include <tuple>

int main()
{
    auto t1 = std::make_tuple("one", "two", "three");
    auto t2 = std::make_tuple("one", "two", "three");
    
    std::cout << "(t1 == t2) is " << std::boolalpha << (t1 == t2) << "\n";
    std::cout << "(t1 != t2) is " << std::boolalpha << (t1 != t2) << "\n";

    return 0;
}

When I run it, I see the following output:

(t1 == t2) is false
(t1 != t2) is true

The tuples are identical, so why does it have wrong comparison results? How do I fix this?

Bernhard Barker
  • 54,589
  • 14
  • 104
  • 138
AeroSun
  • 2,401
  • 2
  • 23
  • 46

4 Answers4

65

You are comparing pointers to buffers of characters, not strings.

Sometimes the compiler will turn two different "one"s into the same buffer, sometimes it will not.

In your case, it isn't. Probably a debug build.

Add #include <string_view>, then

using namespace std::literals;

auto t1 = std::make_tuple("one"sv, "two"sv, "three"sv);
auto t2 = std::make_tuple("one"sv, "two"sv, "three"sv);

and you'll get what you expect. (In pre- compilers, use <string> and ""s instead of <string_view> and ""sv).

Quentin
  • 62,093
  • 7
  • 131
  • 191
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 18
    I can't help but think the moral of the story here is "Don't use `auto` if you don't know what type you are assigning." – chepner Sep 04 '20 at 13:03
  • 9
    @chep rather, it is that `""` are C legacy strings and are really annoying. The fact that two textually identical literals are implementation defined equal is ridiculous. – Yakk - Adam Nevraumont Sep 04 '20 at 14:16
  • 1
    @Yakk-AdamNevraumont You could make a case for forcing them to be merged if in the same TU, but going beyond that can easily get expensive. Going the other way would lead to bloat. – Deduplicator Sep 04 '20 at 16:21
  • 4
    @Deduplicator No, the problem is that `""` is an array literal, and `==` on array literals decays to pointer and compares pointers, which is all legacy C cruft in C++. Merging strings is a red herring; the address of `"hello"` should matter as much as the address of `7`. Decay-to-pointer was a hack when it was invented in C, and array literals not comparing `==` was a missing feature; nobody would write that in a language when they knew the consequences. For backwards compatibility we are stuck with it. – Yakk - Adam Nevraumont Sep 05 '20 at 00:09
  • 1
    @Yakk Not that we have `==` with two string-literals here, but yes, decaying both arguments to a binary operator is a bit much. It would be nice if arrays were first-class, yes, `std::array` is just a rough band-aid. That would also change *array decay* to just another standard conversion, probably forced for non-template vararg. – Deduplicator Sep 05 '20 at 12:24
37

What is the type of "one"? This is not a string, but rather a string literal.

Your problem basically boils down to this code:

char const* a = "one";
char const* b = "one";

std::cout << "(a == b) is " << std::boolalpha << (a == b) << "\n";
std::cout << "(a != b) is " << std::boolalpha << (a != b) << "\n";

Which will most likely output the same result.

This is because a string literal will decay into a char const*. Comparing two pointer compares their location in memory. Now this is a matter of whether your compiler is folding string literals into one. If the string literals are folded, then they are gonna be equal, if they are not, they are not gonna be equal. This can vary with different optimization levels.

How can you fix your comparison then?

Preferably use std::string_view as you don't seem to need to own or change their content:

using namespace std::literals;

// ... 

auto t1 = std::make_tuple("one"sv, "two"sv, "three"sv);
auto t2 = std::make_tuple("one"sv, "two"sv, "three"sv);

The std::string_view class is a thin wrapper around a pointer and a size, and define a comparison operator that check for value equality.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • 1
    I was surprised to see that with I got "(a == b) is true" even when compiling with `gcc -fno-merge-constants`, (same with tuples). Guess that flag is more of a suggestion than a requirement. – rtpax Sep 04 '20 at 18:34
13

The problem is unrelated to C++20, but comes from how string literals are implemented. The answer is for example here:

Why do (only) some compilers use the same address for identical string literals?

In short, your program falls into the category of "undefined unspecified behavior", as it assumes that identical C-style string literals have identical addresses. This is because expressions like "a" == "a" compare addresses, not the content. Your code could be made safe and predictable if you used std::string literals, like "one"s, "one"sv etc., see https://en.cppreference.com/w/cpp/string/basic_string/operator%22%22s

zkoza
  • 2,644
  • 3
  • 16
  • 24
6

auto is not always your friend. I would argue the proper way to get reliably the “right” behaviour without boilerplate is to explicitly use a type that you know has value-equality. Then you can also omit the make_tuple and simply use the initialiser-list constructor:

#include <string>
#include <tuple>
#include <iostream>

typedef std::tuple<std::string, std::string, std::string> StrTriple;

int main() {
  
  StrTriple t1{"one", "two", "three"};
  StrTriple t2{"one", "two", "three"};

  std::cout << "(t1 == t2) is " << std::boolalpha << (t1 == t2) << "\n";
  std::cout << "(t1 != t2) is " << std::boolalpha << (t1 != t2) << "\n";

    return 0;
}

No doubt some would argue that the memory management of std::string incurs unnecessary overhead. string_view may be preferrable, however chances are in a real-world application the strings will need to be dynamically allocated anyway somewhere.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • 1
    `std::tuple` *does* have value-equality. Unfortunately, the value which is compared isn't quite the value you wanted to compare... – Deduplicator Sep 04 '20 at 16:23
  • 2
    @Deduplicator `std::tuple` is not a type, so it doesn't make sense to say it has value equality. `tuple` does, `tuple` doesn't – both of these are types, while `std::tuple` itself is only a _type constructor_. – leftaroundabout Sep 04 '20 at 22:34
  • 1
    Ok, more explicit: `std::tuple` has value-equality if all its arguments have value-equality. It's just that you don't actually want to compare the values of the arguments, but the values of the strings they point to. – Deduplicator Sep 05 '20 at 00:55