unconventional uses of friend in c++
Asked Answered
C

1

10

I know the general use cases for the friend keyword with regards to encapsulation but one a couple of occasions, I have needed the friend keyword just to "get the job done". These use cases don't make me happy so I'm wondering if there are some alternatives. Here's the first minimal example:

struct Foo{
  enum class Bar{
    a=1,b=2,c=4
  };

  // need to tell the compiler of operator| before it gets used
  // but it can't be a member function of Foo: so add friend keyword  
  friend Bar operator|(const Bar& b1, const Bar& b2);  

  // constructor needs a default value using 
  // operator| for Bars 
  Foo( Bar b = Bar::a | Bar::b );
};

// definition of operator|, etc.

Is there any way for the compiler to see the declaration of the operator| for the nested class, inside of interface, before the default values are given at the Foo constructor declaration?

I also sometimes find myself using the friend keyword in defining symmetric operations of nested classes within templates. For example:

template<typename T>
struct A{
  struct B{
    friend bool operator==(const B& x, const B& y) { return true; }  
  };  
};

The operator== does not require friendship from an encapsulation perspective. But due to operator== not actually being a templated function and the compiler being unable to deduce the types of nested classes within templates, this seems to be the only reasonable "trick" to keep operator== as a free function.

As I said, these choices do work, but I'm wondering if there are better choices/practices out there.

Christology answered 20/12, 2021 at 6:43 Comment(4)
Such usages are not really unconventional. friend gives you an additional benefit, sometimes very important - these function become "hidden" friends.Crewel
@Crewel thanks for confirming this can be done! C++ is such an expressive language I just thought there might be another more conventional way to handle these cases, without also giving access to private members.Christology
One way - which does away with the need for the operator|() entirely, is to specify Foos constructor with a default argument in the form Foo( Bar b = Bar(int(Bar::a) | int(Bar::b))). Another ways is to add an enum value to Bar named a_bitor_b = a | b, and change the default argument of Foos constructor to be Foo(Bar b = Bar::a_bitor_b). Then, the declaration of the operator|() can be completely outside the definition of Foo, as long as you define it consistently (e.g. Foo::Bar operator|(const Foo::Bar &a, const Foo::Bar &b) {...}Parous
Ah, that's also a clever workaround. Thanks, @Peter.Christology
A
4

In fact, I would say it's perfectly conventional. While, as mentioned by Evg, hidden friends have special benefits, visible friends are also great to have!

To make this more of an answer, consider for example libstdc++'s implementation of std::unreachable_sentinel_t. This can be returned as the end() of an unbounded "generator range", such as a std::ranges::iota_view{0}. It's very similar to your second example:

struct unreachable_sentinel_t
{
    template<weakly_incrementable _It>
    friend constexpr bool
    operator==(unreachable_sentinel_t, const _It&) noexcept
    { return false; }
};

inline constexpr unreachable_sentinel_t unreachable_sentinel{};

Regular iterators and sentinels are defined as nested classes and also rely on friendship, though their comparison operators typically do need the privileged access. However, even if you could provide an out-of-line definition, an additional benefit of inlining friends in templates is that you don't have to repeat the exact template header, including potentially complicated constraints.

If it helps, in C++ you can think of free functions like these, whether or not they are friends, as being part of the public interface of the classes of their arguments, due to argument-dependent lookup. As such, even if you don't technically need the privilege of friendship, granting it does not break encapsulation.

As long as you're aware of the nuance of hidden friendship, just use friend if it gets the job done!

Athletic answered 21/12, 2021 at 0:14 Comment(1)
This is an excellent answer. Thanks for expounding upon this topic and clearing up my doubts.Christology

© 2022 - 2024 — McMap. All rights reserved.