在 C++ 中实现包含有效负载的类的最佳做法

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

本文关键字:最佳 负载 有效 C++ 实现 包含      更新时间:2023-10-16

我有一个关于层次结构、引用和指针的问题......当我尝试做以下事情时,我想到了这个问题:

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 */    
}

现在想象一下,我有很多类型的有效载荷,所有这些有效载荷都是超类(通用)有效载荷的子级。

我想读取标题并切换命令。 例如,如果命令为 1,我创建一个PayloadReset : Payload并填写其所有属性,然后我想在我的数据包上设置此有效负载(向上投射)。在程序的其他部分,我想读取我当前的数据包,然后读取命令字段并根据命令字段向下转换为适当的类型。

当我尝试这样做时,

我可以毫无问题地进行向上转换,但是当我尝试对特定有效负载进行向下转换时,问题就出现了,在我们的示例中 PayloadReset。

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

Payload *p;

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

Payload p;

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

Payload *p;

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

class PayloadReset : public Payload { /* Class declaration */ };
PayloadReset *r = new PayloadReset( /* Constructor argument */ };
p=r;

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

标记到山姆的答案。

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

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

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

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

首先,您应该花点时间熟悉 Java 命名约定。您可以在网络上找到常用的建议。下面是 Java 命名约定的一个示例。我提出这个是因为单个变量名称通常不是一个好主意,并且随着程序规模的扩大,特别是如果团队中有多个人,使用描述性变量名称会带来红利。因此,与其使用Payload p Payload payload .

其次,在OO(面向对象)中,最好始终将类实例变量保密,而不是公共。仅在必要时授予对这些变量的访问权限,并通过提供公共方法来屏蔽对这些变量的访问。所以,在你的类Packet的例子中,你的公共/私有是倒退的。您的类应该看起来更像:

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;
    }
}

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

您的最后一个问题/主题实际上又是关于OO和类层次结构的。似乎有效负载将是一个通用基类,您可能有多个特定的"有效负载类型",例如ResetPayload。如果是这种情况,则需要定义Payload并创建扩展PayloadResetPayload 类。我不确定您要做什么,但是将类/对象广告名词和方法视为动词。还要考虑"is-a"和"has-a"的概念。据我所知,也许Payload所有的"has-a command and an address. Also, maybe each Payload also has multiple Packet s, whatever. Just as an example, you would then define your Payload"类都是这样的:

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),您将创建类、扩展Payload并提供特定于此类型的其他属性/操作,如下所示:

public class ResetPayload extends Payload{
     public ResetPayload(int addy, int comm){
         super(addy, comm);
     }
     public void reset(){
         //Do stuff here to reset the payload
     }
}

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

这是我对一般问题的看法,它扩展了标记的联合思想。优点是 1.)没有继承/dynamic_cast 2.)没有共享的 PTR 3.)豆荚 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_;
};

请填写所有不错的会员功能。此类仅移动。您正在通过静态成员函数创建具有有效负载的消息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-> ...
 }

只需检查标记是否包含预期的数据(类型),然后将数据转换为指针类型。

免责声明:它不是完整的类。此处缺少某些访问成员函数。