我可以在C++中实现一个自治的`self`成员类型吗?

Lig*_*ica 99 c++ c++11

C++ 缺乏的等效PHP的self关键字,其评估的封闭类的类型.

在每个类的基础上伪造它很容易:

struct Foo
{
   typedef Foo self;
};
Run Code Online (Sandbox Code Playgroud)

但我不得不再写Foo一遍.也许有一天我会弄错,导致一个无声的错误.

我可以使用一些decltype朋友和朋友的组合来"自主地"完成这项工作吗?我已尝试过以下内容this在该地方无效:

struct Foo
{
   typedef decltype(*this) self;
};

// main.cpp:3:22: error: invalid use of 'this' at top level
//     typedef decltype(*this) self;
Run Code Online (Sandbox Code Playgroud)

(我不会担心相同的static,相同但后期绑定.)

Seb*_*ann 38

一种可能的解决方法(因为您仍需要编写一次类型):

template<typename T>
struct Self
{
protected:
    typedef T self;
};

struct Foo : public Self<Foo>
{
    void test()
    {
        self obj;
    }
};
Run Code Online (Sandbox Code Playgroud)

为了更安全的版本,我们可以确保T实际上来自Self<T>:

Self()
{
    static_assert(std::is_base_of<Self<T>, T>::value, "Wrong type passed to Self");
}
Run Code Online (Sandbox Code Playgroud)

请注意,static_assert成员函数内部可能是检查的唯一方法,因为传递的类型std::is_base_of必须完整.

  • 然而,它比原始方法稍微好一点,因为重复是非常接近的.不是问题的解决方案,而是为最佳案例解决方案的值得尝试+1. (6认同)
  • typedef中不需要`typename`.由于这不会减少冗余数量,我认为这不是一个可行的替代方案. (4认同)
  • 我曾经使用过这个解决方案几次,它有一个不好的东西:当后来从`Foo`派生时,你必须要么:(1)将T向上传播到叶子后代,或者(2)记得继承自SelfT很多次,或者(3)接受所有孩子的东西都是基地..可用,但不成熟. (4认同)

Ral*_*zky 38

以下是如何在不重复Foo类型的情况下执行此操作:

template <typename...Ts>
class Self;

template <typename X, typename...Ts>
class Self<X,Ts...> : public Ts...
{
protected:
    typedef X self;
};

#define WITH_SELF(X) X : public Self<X>
#define WITH_SELF_DERIVED(X,...) X : public Self<X,__VA_ARGS__>

class WITH_SELF(Foo)
{
    void test()
    {
        self foo;
    }
};
Run Code Online (Sandbox Code Playgroud)

如果要从中派生,Foo则应按WITH_SELF_DERIVED以下方式使用宏:

class WITH_SELF_DERIVED(Bar,Foo)
{
    /* ... */
};
Run Code Online (Sandbox Code Playgroud)

您甚至可以根据需要使用尽可能多的基类进行多重继承(感谢可变参数模板和可变参数宏):

class WITH_SELF(Foo2)
{
    /* ... */
};

class WITH_SELF_DERIVED(Bar2,Foo,Foo2)
{
    /* ... */
};
Run Code Online (Sandbox Code Playgroud)

我已经验证了这可以用于gcc 4.8和clang 3.4.

  • 我想答案是"不,但是拉尔夫可以!" ;) (18认同)
  • @MilesRout这是一个关于问题的问题,而不是答案.在软件开发(尤其是维护)的许多情况下,避免代码中的冗余是有帮助的,因此在一个地方更改内容不需要您在其他地方更改代码.这就是`auto`和`decltype`的全部意义,或者在这种情况下是`self`. (7认同)
  • 这有什么优于简单地将typedef放在那里?天哪,为什么你甚至需要typedef?为什么? (3认同)
  • 因此,不是重新键入类型,而是必须用丑陋的宏来混淆声明?很优雅的解决方案 (3认同)
  • `template&lt;typename T&gt;class Self{protected: typedef T self;}; class WITH_SELF(Foo) : public Bar, private Baz {};` 本来会更简单,并且可以更精确地控制继承 - 有什么反对的理由吗? (2认同)

Bar*_*icz 33

您可以使用宏而不是常规类声明,这将为您执行此操作.

#define CLASS_WITH_SELF(X) class X { typedef X self;
Run Code Online (Sandbox Code Playgroud)

然后使用喜欢

CLASS_WITH_SELF(Foo) 
};
Run Code Online (Sandbox Code Playgroud)

#define END_CLASS }; 可能有助于提高可读性.


您也可以使用@ Paranaix Self并使用它(它开始变得非常hackish)

#define WITH_SELF(X) X : public Self<X>

class WITH_SELF(Foo) {
};
Run Code Online (Sandbox Code Playgroud)

  • @DeadMG我认为有些人可能会更加一致; 毕竟,第一个宏的使用并不以`{`结尾,所以`}`是"悬挂",文本编辑也可能不喜欢它. (31认同)
  • EWWWW END_CLASS.这完全没必要. (18认同)
  • 不错的想法,但即使我并没有从根本上反对宏,我只会接受它的用法,如果它模仿C++范围,即它是否可用作`CLASS_WITH_SELF(foo){...};` - 我认为这是不可能实现的. (6认同)
  • @KonradRudolph我也添加了一种方法.不是我喜欢它,只是为了完整 (2认同)

Kon*_*lph 31

我没有正面证据,但我认为这是不可能的.以下失败 - 出于与您的尝试相同的原因 - 我认为这是我们能得到的最远的:

struct Foo {
    auto self_() -> decltype(*this) { return *this; }

    using self = decltype(self_());
};
Run Code Online (Sandbox Code Playgroud)

从本质上讲,这表明我们想要声明我们的typedef的范围根本没有访问权限(无论是直接的还是间接的)this,并且没有其他(编译器独立的)方式来获取类的类型或名称.

  • 这可能与C++ 1y的返回类型推论有关吗? (4认同)
  • @dyp为了我的答案,不会改变任何东西.这里的错误不在尾随返回类型中,而是在调用中. (4认同)
  • FWIW,`struct S {int i; typedef decltype(i)Int; 即使`i`是非静态数据成员,``也可以工作.它的工作原理是因为`decltype`有一个特殊的例外,其中一个简单的名称不被评估为表达式.但我无法想出以某种方式回答这个问题的方式. (4认同)

小智 21

在GCC和clang中都有用的是创建一个typedef,它this通过this在函数typedef的trailing-return-type中使用来引用.由于这不是静态成员函数的声明,因此this可以容忍使用.然后,您可以使用该typedef进行定义self.

#define DEFINE_SELF() \
    typedef auto _self_fn() -> decltype(*this); \
    using self = decltype(((_self_fn*)0)())

struct Foo {
    DEFINE_SELF();
};

struct Bar {
    DEFINE_SELF();
};
Run Code Online (Sandbox Code Playgroud)

不幸的是,严格阅读标准说即使这是无效的.clang做的是检查this静态成员函数的定义中没有使用的.在这里,它确实不是.GCC并不介意是否this在尾随返回类型中使用它而不管函数的类型如何,它甚至允许它用于static成员函数.但是,标准实际要求的this是不在非静态成员函数(或非静态数据成员初始化程序)的定义之外使用.英特尔做对了并拒绝这一点.

鉴于:

  • this 仅允许在非静态数据成员初始化器和非静态成员函数([expr.prim.general] p5)中,
  • 非静态数据成员不能从初始化程序推导出它们的类型([dcl.spec.auto] p5),
  • 非静态成员函数只能在函数调用的上下文中用非限定名称引用([expr.ref] p4)
  • 非静态成员函数只能通过非限定名称调用,即使在未this评估的上下文中,也可以使用([over.call.func] p3),
  • 通过限定名称或成员访问权限引用非静态成员函数需要引用所定义的类型

我想我可以确定地说,如果self不以某种方式包含类型名称,就没有办法实现.

编辑:我之前的推理有一个缺陷."非静态成员函数只能通过非限定名称调用,即使在未评估的上下文中,当可以使用它时([over.call.func] p3),"是不正确的.它实际上是说什么

如果关键字this(9.3.2)在范围内并且引用了类T或派生类T,那么隐含的对象参数是(*this).如果关键字this不在范围内或引用另一个类,则类型的设计对象将T成为隐含的​​对象参数.如果参数列表由设计对象增强并且重载决策选择其中一个非静态成员函数T,则调用是格式错误的.

在静态成员函数内部,this可能不会出现,但它仍然存在.

但是,根据注释,在静态成员函数内部,将不执行f()to 的转换(*this).f(),并且未执行转换,则违反[expr.call] p1:

[...]对于成员函数调用,后缀表达式应为隐式(9.3.1,9.4)或显式类成员访问(5.2.5),其[...]

因为没有会员访问权限.所以即使这样也行不通.


Yak*_*ont 17

#define SELF_CHECK( SELF ) void self_check() { static_assert( std::is_same< typename std::decay<decltype(*this)>::type, SELF >::value, "self wrong type" ); }
#define SELF(T) typedef T self; SELF_CHECK(T)

struct Foo {
  SELF(Foo); // works, self is defined as `Foo`
};
struct Bar {
  SELF(Foo); // fails
};
Run Code Online (Sandbox Code Playgroud)

这不适用于模板类型,因为self_check未调用,因此static_assert不进行评估.

我们可以做一些黑客攻击以使其适用于templates,但它的运行时间成本较低.

#define TESTER_HELPER_TYPE \
template<typename T, std::size_t line> \
struct line_tester_t { \
  line_tester_t() { \
    static_assert( std::is_same< decltype(T::line_tester), line_tester_t<T,line> >::value, "test failed" ); \
    static_assert( std::is_same< decltype(&T::static_test_zzz), T*(*)() >::value, "test 2 failed" ); \
  } \
}

#define SELF_CHECK( SELF ) void self_check() { static_assert( std::is_same< typename std::decay<decltype(*this)>::type, SELF >::value, "self wrong type" ); }

#define SELF(T) typedef T self; SELF_CHECK(T); static T* static_test_zzz() { return nullptr; }; TESTER_HELPER_TYPE; line_tester_t<T,__LINE__> line_tester
Run Code Online (Sandbox Code Playgroud)

struct在您的类中创建一个大小为1字节的空.如果您的类型被实例化,self则进行测试.

  • @LightnessRacesinOrbit哦,其实我是.谢谢,这将为我节省一些打字:).我总是对我对C++的了解程度感到惊讶. (2认同)

Dan*_*rey 11

我也认为这是不可能的,这是另一个失败但恕我直言的有趣尝试,它避免了 - this访问:

template<typename T>
struct class_t;

template<typename T, typename R>
struct class_t< R (T::*)() > { using type = T; };

struct Foo
{
   void self_f(); using self = typename class_t<decltype(&self_f)>::type;
};

#include <type_traits>

int main()
{
    static_assert( std::is_same< Foo::self, Foo >::value, "" );
}
Run Code Online (Sandbox Code Playgroud)

失败的原因self_f是,当你想要获取它的地址时,C++要求你符合该类的资格:(


小智 8

我最近发现*this允许使用支撑或等于初始化器.在第5.1.1节(来自n3337工作草案)中描述:

3 [..]与其他上下文中的对象表达式不同*this,为了成员函数体之外的类成员访问(5.2.5),不要求它是完整类型.[..]

4否则,如果member-declarator声明类X的非静态数据成员(9.2),则表达式this是可选的brace-or-equal-initializer中类型为"指向X的指针"的prvalue .它不应出现在成员声明者的其他地方.

5该表达this不得出现在任何其他背景下.[ 例如:

class Outer {
    int a[sizeof(*this)];               // error: not inside a member function
    unsigned int sz = sizeof(*this);    // OK: in brace-or-equal-initializer

    void f() {
        int b[sizeof(*this)];           // OK
        struct Inner {
            int c[sizeof(*this)];       // error: not inside a member function of Inner
        };
    }
};
Run Code Online (Sandbox Code Playgroud)

- 结束例子 ]

考虑到这一点,以下代码:

struct Foo
{
    Foo* test = this;
    using self = decltype(test);

    static void smf()
    {
        self foo;
    }
};

#include <iostream>
#include <type_traits>

int main()
{
    static_assert( std::is_same< Foo::self, Foo* >::value, "" );
}
Run Code Online (Sandbox Code Playgroud)

通过丹尼尔弗雷的 static_assert.

Live example

  • 我们在这里肯定没有得到任何东西,因为我们必须声明“test”的类型,嗯,“Foo *”! (2认同)

Hol*_*Cat 7

终于找到合适的解决办法了!这个想法是由github 上的 @MitalAshok提出的。

#include <type_traits>

namespace SelfType
{
    template <typename T>
    struct Reader
    {
        friend auto adl_GetSelfType(Reader<T>);
    };

    template <typename T, typename U>
    struct Writer
    {
        friend auto adl_GetSelfType(Reader<T>){return U{};}
    };

    inline void adl_GetSelfType() {}

    template <typename T>
    using Read = std::remove_pointer_t<decltype(adl_GetSelfType(Reader<T>{}))>;
}

#define DEFINE_SELF \
    struct _self_type_tag {}; \
    constexpr auto _self_type_helper() -> decltype(::SelfType::Writer<_self_type_tag, decltype(this)>{}, void()) {} \
    using Self = ::SelfType::Read<_self_type_tag>;
Run Code Online (Sandbox Code Playgroud)

然后:

struct A
{
    DEFINE_SELF
    static_assert(std::is_same_v<Self, A>);
};
Run Code Online (Sandbox Code Playgroud)

这使用有状态模板元编程将类型存储在可访问的上下文中(在辅助函数的尾随返回类型中),然后读取否则无法访问的类型(在类范围内)。

这里的关键是 writer 是如何实例化的。简单地这样做auto _self_type_helper() -> Writer<...> {return {};}是行不通的:编写器被实例化时有延迟,使得状态可以在任何成员函数体内访问,但不能在类范围内访问。

但是,如果您这样做-> decltype(Writer<...>{}, void()),或者以其他方式使编写器成为影响类型的表达式的一部分,那么它就会开始工作。我不完全确定为什么会发生这种情况。

  • 伟大的!实际上可以省略 `, void()` 和成员函数体,演示:https://gcc.godbolt.org/z/rrb1jdTrK (2认同)