C++友元函数问题

2020-08-19 05:13:53 +08:00
 Tony042

先上代码,注意 get 函数和其友元函数

#include <utility>
#include <type_traits>

template <unsigned Height, typename T, bool = std::is_class_v<T> && !std::is_final_v<T>>
class TupleElt;

template <unsigned Height, typename T>
class TupleElt<Height, T, false>
{
private:
    T value;

public:
    TupleElt() = default;
    template <typename U>
    TupleElt(U &&other) : value(std::forward<U>(other)) {}
    T &get() { return value; }
    T const &get() const { return value; }
};

template <unsigned Height, typename T>
class TupleElt<Height, T, true> : private T
{
public:
    TupleElt() = default;
    template <typename U>
    TupleElt(U &&other) : T(std::forward<U>(other)) {}
    T &get() { return *this; }
    T const &get() const { return *this; }
};

template <unsigned H, typename T>
T &getHeight(TupleElt<H, T> &te)
{
    return te.get();
}

template <typename... Types>
class Tuple;

template <unsigned I, typename... Elements>
auto get(Tuple<Elements...> &t) -> decltype(getHeight<sizeof...(Elements) - I - 1>(t))
{
    return getHeight<sizeof...(Elements) - I - 1>(t);
}


template <typename... Types>
class Tuple;

template <typename Head, typename... Tail>
class Tuple<Head, Tail...> : private TupleElt<sizeof...(Tail), Head>, private Tuple<Tail...>
{
    template <unsigned I, typename... Elements>
    friend auto get(Tuple<Elements...> &t) -> decltype(getHeight<sizeof...(Elements) - I - 1>(t));  \\ 友元函数声明

private:
    using HeadElt = TupleElt<sizeof...(Tail), Head>;

public:
    Head &getHead() { return static_cast<HeadElt *>(this)->get(); }
    Head const &getHead() const { return static_cast<HeadElt const *>(this)->get(); }
    Tuple<Tail...> &getTail() { return *this; }
    Tuple<Tail...> const &getTail() const { return *this; }
};

template <>
class Tuple<>
{
};

int main()
{
    Tuple<char, char> t1;
    get<0>(t1);
    return 0
}

我跟着 C++ templates 2nd edition, 实现了一个简单的 tuple 类和 get 函数,其中 tuple 类里面的元素被 tuple 类私有继承,get 函数通过计算元素高度,来实现直接从派生类到基类的转换来访问对应的元素,由于 tupleElt 类被私有继承,get 函数必须在 tuple 类中被声明为友元函数才可以被访问(上述代码注释处)但是即使被声明为了友元函数,gcc 还是提示 get 函数无法访问 tuple 的基类,导致编译错误,但 MSVC 可以顺利编译通过,是为什么呢,如果有错误该怎么改?

2422 次点击
所在节点    C++
10 条回复
Tony042
2020-08-19 05:36:49 +08:00
刚才又试了下,clang-10, MSVC 16.7.0 都可以编译通过,gcc-9, gcc-10 都编译不通过...
Tony042
2020-08-19 10:01:33 +08:00
果然 C++的回答就是少啊,自顶一下,另外题中代码可以通过这个链接调试 https://godbolt.org/z/Wr4arz
codehz
2020-08-19 10:34:44 +08:00
虽然不知道发生了什么,但是你改成这样就能通过编译 https://godbolt.org/z/bq8sEn
另外你这个方法做 tuple 居然还需要 static_cast,这肯定和正确的做法有所偏离。。。
Tony042
2020-08-19 10:45:07 +08:00
@codehz 同不知道发生了什么,友元函数不行,友元类却可以,gcc 这行为挺奇怪的。stl 里面的 get 函数就是通过 static_cast 获得 tuple 里元素的值。《 C++ templates 》这本书代码有点坑,好多地方都有小错误,或者不太稳定
Tony042
2020-08-19 10:49:04 +08:00
@codehz anyway 谢谢老哥的回复啊,基本上我的每个问题都是你帮我回答的,谢谢啦
Wirbelwind
2020-08-19 11:18:37 +08:00
大概是编译器实现问题?

使用 private 类型 继承的类型,就算是被继承类型中的 public 成员也是不能访问的。

写个简化版的 demo 的话,clang 和 msvc 也是不给过的。

简单的解决办法就是 private TupleElt 改成 public TupleElt
Tony042
2020-08-19 11:27:24 +08:00
@Wirbelwind 应该是编译器实现问题吧,我查了下 stl 源码,msvc 的方式是将 get 函数声明为友元函数,gcc 是声明友元类。 在这个 case 里面,由于每个 tuple 类的实现都声明了所有 get 函数为友元函数,其实按理说 private 不会影响 get 函数做 Derived->Base 转换的
constexpr
2020-08-19 18:38:21 +08:00
我研究了一下,我觉得这个问题大概跟友元有关,为此我写了一段极短的代码

struct Base { void f() {} };

void m(Base*) {}

template<typename T>auto v2ex(T* b) -> decltype(m(b));

struct Derived : private Base {
template<typename T> friend auto v2ex(T* d) -> decltype(m(d)); //problem HERE!!!
};

template<typename T> auto v2ex(T* d) -> decltype(m(d)) { d->f(); }

int main() {
Derived d;
v2ex(&d);
}

这段代码能在 MSVC, clang 中编译通过并运行, g++无法编译.

这段代码的问题在于, 模板函数 V2EX 是类 Derived 的友元, 故可以在 V2EX 中自由转换成类 Base. 但是 V2EX 的声明有这么一段 "decltype(m(b))" , 问题是函数 m 是否应该是类 Derived 的友元呢? 如果是的话, 我传给他一个类 Dervied 的指针,他应该在 V2EX 中能畅通无阻的转换成类 Base, clang 和 MSVC 都认为是的,所以编译通过, 而 g++认为 m 不是 Derived 的友元, m 中不允许 Derived->Base 的转换, 这就是他无法编译的原因.
对应你的代码就是 g++不认为 getHeight 是 Tuple 的友元, 所以在友元声明中 "getHeight<sizeof...(Elements) - I - 1>(t)", g++不允许 Tuple 类型的参数 t 向基类 TupleElt 的转换!!!

有几点说明一下:
■ 代码也许可以写的更短, 但是为了恰好在 MSVC 和 clang 下编译成功, 而 gcc 不成功才写的稍复杂了一点.
■ 你的代码比我这段代码复杂的多, 而且涉及递归继承, 多重继承. 虽然我不敢肯定我提到的这个问题就一定是你问题的解答, 但从 g++编译器给出的错误提示中, 大致就是我提出的这个问题.
■ 至于 ISO 标准是怎么样的, 我不清楚. 不过也能看出不同编译器对标准的解读,实现都有偏差.
constexpr
2020-08-19 18:42:12 +08:00
@constexpr V2EX 好像自动变大写了!~
Wirbelwind
2020-08-20 10:55:09 +08:00
@constexpr 这样理解确实不错,之前我试了在 base class 加友元函数,但是出现 ambigous 问题 就作罢了

但是编译器报的错误是 inaccessible base of TupleElt,也可能是 gcc 不允许访问 private

友元一定程度上破坏了 oop 特性

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/699474

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX