1

What are available options to separate different instances of a class at compile time?

I basically want the following code snippet to compile without errors:

struct equal_ret_t {};
struct different_ret_t {};

struct Foo
{
    Foo() = default;
    // ... add relevant code here
};

constexpr auto eval(Foo const& a, Foo const& b)
{
    // return equal_ret_t for same instances,
    // different_ret_t for different instances
}

int main()
{
    Foo a;
    Foo b;
    static_assert(std::is_same_v<decltype(eval(a,a)),equal_ret_t>);
    static_assert(std::is_same_v<decltype(eval(b,b)),equal_ret_t>);
    static_assert(std::is_same_v<decltype(eval(a,b)),different_ret_t>);
}

What is the required code to add to the Foo class and/or the eval function?

Are there any solutions approved by the current C++20/23 standard? If not, can the solution made portable across major compilers?

davidhigh
  • 14,652
  • 2
  • 44
  • 75
  • `equal_ret_t{}` is an expression, not a type, so you can't use it this way in `std::is_same_v`. After fixing that it is still impossible because `a` and `b` are not static storage duration objects. You can't have the type of `eval(a,a)` (a compile-time property) depend on properties that are only known at runtime and functions can also not have a return type dependent on the _values_ of function arguments, instead template arguments would need to be used. What do you need this for? – user17732522 Jul 31 '22 at 21:46
  • 1
    A function, given the same parameter types, cannot have multiple return types. You can return a `std::variant`, but at that point you might as well just return a type like `bool` because only the return value can be different, not the return type. – Patrick Roberts Jul 31 '22 at 21:46
  • 2
    to tell if two objects of the same type are the same object, you check if they have the same address. If you want to do that at compile time you cant use block scope variables. – NathanOliver Jul 31 '22 at 21:49
  • @user17732522: sure, there must be types in `std::is_same_v` fixed that. Otherwise, I see several solutions, either using the `__COUNTER__` macro or along the lines of [this question](https://stackoverflow.com/questions/60082260/c-compile-time-counters-revisited) – davidhigh Jul 31 '22 at 22:02
  • @davidhigh I don't see how either help you here. Please explain how exactly you want to use this. Otherwise I have to make assumptions that may not match what you are thinking about. – user17732522 Jul 31 '22 at 22:02
  • @user17732522: they could plant a *changing* compile time id into a class instance, which could be used as a basis for comparison – davidhigh Jul 31 '22 at 22:10
  • @davidhigh No, because following all references to the objects would require evaluation of the whole program. My question is what exactly you want to compare for equality. Sure, comparing specifically two variable declarations by name in a function like this is possible, but if you have e.g. only references to the objects, it will not work. – user17732522 Jul 31 '22 at 22:14
  • @user17732522: ok, I see your point, one could set up a random generator returning a reference to one or the other object. This surely can't be evaluated at compile time. So let's assume a `constexpr`-access ... meaning all you need to know to evaluate the instance is available at compile time. – davidhigh Jul 31 '22 at 22:18
  • @davidhigh But then you can (aside from the special cases mentioned previously) only compare the identity of static storage duration objects (i.e. marked `static` or declared at namespace scope). These are the only objects which have addresses that are known at compile-time. If you are fine with this, the given approach in the posted answer always works fine. You just can't have a function return different types based on the values of function arguments, so the answer avoids that by putting the choice in a template argument. – user17732522 Jul 31 '22 at 22:20
  • @user17732522: not sure about that ... I mean, that's basic C++ knowledge which I would have subscribed three weeks ago, but look e.g. at [this library](https://github.com/DaemonSnake/unconstexpr) – davidhigh Jul 31 '22 at 22:29
  • @davidhigh I don't really see how that is related. The library is just using a loophole in the language to introduce compile-time meta state which affects the meaning of individual expressions/statements as the compiler goes top-to-bottom through them. However, from a quick look it seems to me that it is relying on a good bit of undefined or at least unspecified/underspecified behavior for a lot of it, as well. – user17732522 Jul 31 '22 at 22:38
  • @davidhigh the existence of this library does not necessarily contradict what user17732522 is saying. It relies on [non-template friend injection](https://github.com/DaemonSnake/unconstexpr/blob/5892b79efd8c892f2cf2108159aedb227ada6f6e/unconstexpr/uniq_value.hpp#L30-L45), which is generally regarded as a language feature which may not be around forever, due to the WG21's wish to remove any construct from the standard that enables stateful metaprogramming. [GCC has a warning for this construct](https://stackoverflow.com/a/71231202), and even the library itself warns it is "only for fun". – Patrick Roberts Jul 31 '22 at 22:39
  • Both of you are correct in the sense that these are some quirks which might be removed (and were identified as such as early as 2015), but in the end, it seems the task of the question could be fulfilled ... same for the counter macro, probably by using another layer of abstraction around Foo. I was exactly looking for thos kind of quirks, which is why I mentioned conformity to the standard. Should have been more explicit about that. – davidhigh Jul 31 '22 at 22:48

2 Answers2

2

You will not be able to achieve the exact syntax you expect to use, but here's something that may be close enough for your purposes:

#include <type_traits>

struct Foo {
  constexpr bool operator==(const Foo &other) const {
    return this == &other;
  }
};

struct equal_ret_t {};
struct different_ret_t {};

template <bool Equal>
using ret_t = std::conditional_t<Equal, equal_ret_t, different_ret_t>;

int main() {
  Foo a;
  Foo b;
  static_assert(std::is_same_v<ret_t<a == a>, equal_ret_t>);
  static_assert(std::is_same_v<ret_t<b == b>, equal_ret_t>);
  static_assert(std::is_same_v<ret_t<a == b>, different_ret_t>);
}

Try it on Compiler Explorer

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • Amazing, a constexpr equality comparison based on memory addresses is exactly what I was looking for. Since when is this possible? – davidhigh Jul 31 '22 at 22:07
  • @davidhigh [since C++11](https://godbolt.org/z/G8GfMTz14), if you refactor the usage of `std::is_same_v` and `std::conditional_t` which were added in C++17 and C++14 respectively. – Patrick Roberts Jul 31 '22 at 22:10
  • 1
    This works only in very limited circumstances, basically only if the variables are referred to directly by their name, not e.g. through references or pointers. – user17732522 Jul 31 '22 at 22:10
  • What user17732522 says is correct, but the restrictions make sense when you consider that you cannot determine whether two arbitrary runtime objects are the same instance at compile time, not without the necessary context directly in scope. – Patrick Roberts Jul 31 '22 at 22:24
  • Again, when is this possible, and when not? Are there some central requirements which can be stated that constexpr comparison works? – davidhigh Jul 31 '22 at 22:33
1

If you're willing to make Foo a class template, one way of achieving the desired syntax is using non-template friend injection*.

The following program introduces stateful metaprogramming through a user-defined deduction guide to conceal the fact that a and b are two different types:

#include <type_traits>

namespace detail {

template <std::size_t N>
struct tag {
  friend auto get(tag);

  template <class = tag>
  struct set : std::true_type {
    friend auto get(tag) {}
  };
};

template <std::size_t N = 0>
constexpr auto size(auto const& value) {
  if constexpr (requires { get(tag<N>{}); }) {
    return size<N + 1>(value);
  } else {
    return N;
  }
}

}  // namespace detail

struct equal_ret_t {};
struct different_ret_t {};

template <std::size_t N>
struct Foo {
  Foo() = default;

  static_assert(typename detail::tag<N>::set{});
};

template <std::size_t N = detail::size([] {})>
Foo() -> Foo<N>;

constexpr auto eval(auto const& a, auto const& b) {
  return std::conditional_t<std::is_same_v<std::remove_cvref_t<decltype(a)>,
                                           std::remove_cvref_t<decltype(b)>>,
                            equal_ret_t, different_ret_t>{};
}

int main() {
  Foo a;
  Foo b;
  static_assert(std::is_same_v<decltype(eval(a, a)), equal_ret_t>);
  static_assert(std::is_same_v<decltype(eval(b, b)), equal_ret_t>);
  static_assert(std::is_same_v<decltype(eval(a, b)), different_ret_t>);
}

Try it on Compiler Explorer

*You cannot use this technique in a standard-compliant C++ program since it is compiler implementation-dependent, and because stateful metaprogramming is localized to each translation unit, it will very likely violate ODR as a result.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153