在 C++ 中实现 Payload-containing class 的最佳实践?

Best practices to implement a Payload-containing class in C++?

我对层次结构、引用和指针有疑问...当我尝试执行以下操作时,我想到了这个问题:

class packet {
public:
    int address;
    int command; /**< Command select the type of Payload that I must decode */
    Payload p; /**< Generic payload, first question:
                    Payload p or Payload * p or Payload &p ?
                    I have a background in C, for this reason I prefer
                    Payload p but I know that this is not recommended for C++ */

private:
    /** All getter and setter for attributes */
    /** Second question: What is the best way to implement a getter
        and setter for Payload?... I prefer something
        similar to Java if this is possible */    
}

现在假设我有很多类型的有效载荷,所有这些有效载荷都是超级 class(通用)有效载荷的 children。

我想阅读 header 并关闭命令。例如,如果命令是 1 我创建一个 PayloadReset : Payload 并填写它的所有属性,那么我想 set 在我的数据包上这个有效载荷(up-casting) .在程序的其他部分,我想读取我当前的数据包,然后读取命令字段和 down-cast 到适当的类型,具体取决于命令字段。

当我尝试这样做时,我可以毫无问题地执行 up-casting,但是当我尝试向下转换到特定的有效负载时,在我们的示例 PayloadReset 中出现了问题。

回答第一个问题(隐藏在您第一个代码示例的注释中:

Payload *p;

在从 Java 过渡到 C++ 的过程中,您需要学习的第一件事是指针是什么以及它们如何工作。一段时间以来,您会感到困惑的是,Java 中的所有对象实际上都是指针。在使用 Java 时,您永远不需要知道这一点。但是你现在必须知道这一点,才能理解 C++。因此,将 C++ class 声明为

Payload p;

与Java中的类似声明不同。 Java 中没有与此声明等效的内容。在 Java 中你真的有一个指针,你必须使用 new 关键字实例化它。那部分 Java 最初是从 C++ 模仿而来的。这与 C++ 的过程相同,只是您必须将其显式声明为指针。

Payload *p;

然后,在其他地方,使用您的示例 PayloadReset subclass:

class PayloadReset : public Payload { /* Class declaration */ };

PayloadReset *r = new PayloadReset( /* Constructor argument */ };

p=r;

作为从 Java 到 C++ 的交易的一部分,您需要学习的第二件事是何时以及如何 delete 所有实例化对象。您这里没有 Java 的垃圾收集器。现在这就是你的工作了。

您的问题与 Java 语法有些相关,但主要是关于面向对象编程。

首先,您应该花点时间熟悉 Java 命名约定。您可以在整个网络上找到常用的建议。这是 Java Naming Conventions 的一个示例。我之所以提出这个问题,是因为单个变量名通常不是一个好主意,并且随着程序规模的增长,使用描述性变量名会带来好处,尤其是当团队中有多个人时。所以,而不是 Payload p 使用 Payload payload.

其次,在 OO(面向对象)中,最好始终保持 Class 实例变量私有,而不是 public。仅在必要时授予对这些变量的访问权限,并通过提供 public 方法来屏蔽对它们的访问。因此,在您的 class Packet 示例中,您的 public/private 是倒退的。您的 class 应该更像:

public class Packet{
    //private fields
    private int address;
    private int command;
    private Payload payload;

    //Maybe provide a nice constructor to take in expected
    //properties on instantiation
    public Packet(Payload pay){

    }

    //public methods - as needed
    public void getPayload(){
       return this.payload;
    }

    public void setAddress(int addy){
        this.address = addy;
    }

    public int getCommand(){
       return this.command;
    }
}

另外,多回答你关于Payload命名的问题。就像我之前说的……使用描述性名称。 Java 没有像 C 那样的指针引用,通常会为您处理内存管理,因此不需要或不支持 &

你的最后一个 question/topic 又是关于 OO 和 Class 层次结构的。 Payload 似乎是一个通用的基础 class,您可能有多个特定的 'Payload types',例如 ResetPayload。如果是这种情况,您将定义 Payload 并创建扩展 PayloadResetPayload class。我不确定您到底想做什么,但可以将 Classes/objects 广告名词和方法视为动词。还要考虑 'is-a' 和 'has-a' 的概念。据我所知,也许所有 Payloads 'has-acommand and an address. Also, maybe eachPayloadalso has multiplePackets, whatever. Just as an example, you would then define yourPayload` class 像这样:

public class Payload{
    private int address;
    private int command;
    private List<Packet> packets = new ArrayList<>();

    public Payload(int addy, int comm){
        this.address = addy;
        this.command = comm;
    }

    public void addPacket(Packet p){
        packets.add(p);
    }

    public List<Packet> getPackets(){
        return this.packets;
    }

    public int getCommand(){
        return this.command;
    }

    public int getAddress(){
        return this.address;
    }
}

然后,如果您有一个更具体的 Payload 类型,例如 Reset,您将创建 class、扩展 Payload 并提供额外的 properties/operations特定于此类型,例如:

public class ResetPayload extends Payload{

     public ResetPayload(int addy, int comm){
         super(addy, comm);
     }

     public void reset(){
         //Do stuff here to reset the payload
     }
}

希望这能回答您的问题并让您走得更远。祝你好运。

标记到 Sam 的回答上。

  1. 在继续之前,请了解堆栈和堆分配之间的区别。在您发布的示例中,您在堆栈上分配 Payload p; object - 这意味着 object 的大小此时已知并且所述大小将在堆栈上分配.如果您想将派生的 object 分配给 p,那是行不通的,因为 object 的大小可能不同。这就是为什么你改为声明一个指向 object 的指针(64 位架构上为 8 个字节,32 位架构上为 4 个字节),然后当你知道要分配哪种类型的派生 object 时,您使用 new 运算符来执行此操作,例如:

    Payload *p;
    p = new PayloadReset(...);
    
  2. 上述方法需要手动管理内存,即在 new 分配的指针上调用 delete。从 C++11 开始,建议使用 <memory> header 中的智能指针。这些本质上是引用计数指针,会自动为您调用 delete

    std::shared_ptr<Payload> p;
    p = std::make_shared<PayloadReset>(...);
    

这是我对一般问题的看法,它扩展了标记联合的想法。优点是 1.) 没有 inheritance/dynamic_cast 2.) 没有共享 ptr 3.) POD 4.) rtti 用于生成唯一标签:

using cleanup_fun_t = void(*)(msg*);

class msg
{
public:
    template<typename T, typename... Args>
    static msg make(Args&&... args);
private:
    std::type_index tag_;
    mutable std::atomic<cleanup_fun_t> del_fn_; // hell is waiting for me, 
    uint64_t meta_;
    uint64_t data_;
};

请填写所有nice成员函数。这 class 只是移动。您正在通过静态成员函数 make:

创建带有负载的消息
template<typename T, typename... Args>
msg msg::make(Args&&... args)
{
     msg m;
     m.tag_ = typeid(T);
     m.del_fn_ = nullptr;
     if (!(std::is_empty<T>::value))
     {
         auto ptr = std::make_unique<T>(std::forward<Args>(args)...);
         m.data_ = (uint64_t)ptr.release();
         m.del_fn_ = &details::cleanup_t<T>::fun; // deleter template not shown
     }
     return m;
 }

 // creation:
 msg m = msg::make<Payload>(params passed to payload constructor);
 // using
 if (m.tag() == typeid(Payload))
 {
     Payload* ptr = (Payload*)m.data;
     ptr-> ...
 }

只需检查标签是否包含您期望的数据(类型)并将数据转换为指针类型。

免责声明:这不是完整的 class。此处缺少一些访问成员函数。