【问题标题】:C++ friend subclasses access private members (strategy pattern)C++友元子类访问私有成员(策略模式)
【发布时间】:2019-05-29 15:21:35
【问题描述】:

我为遗传算法编写了一个交叉方法(请参阅https://en.wikipedia.org/wiki/Crossover_(genetic_algorithm))。

交叉方法修改了 Chromosome 类的私有成员,但我将其从 Chromosome 中拉出到单独的纯虚拟基类 CrossoverStrategy(Chromosome 的朋友)中,以使每个交叉方法很好地封装在子类中,即 GoF 策略模式(见https://en.wikipedia.org/wiki/Strategy_pattern)。

现在的问题是 CrossoverStrategy 子类无法访问 Chromosome 私有成员,因为 C++ 中没有继承友谊。我看到的唯一 2 个解决方案是:

1) 将访问器方法添加到纯虚拟基类,例如CrossoverStrategy::getGenes() 使子类可以访问 Chromosome 私有成员。因为 CrossoverStrategy 无法预测其子类可能想要对 Chromosome 做的所有事情,所以我需要预先公开所有内容。丑!

2) 前向声明每个 CrossoverStrategy 子类并明确使其成为 Chromosome 的朋友。这感觉不那么难看,至少保持接口和代码更干净。为了美观,我倾向于这个选项。

有更好的设计建议吗?代码如下。

// Chromosome.h ++++++++++++++++++++++++++++++++++++++++++++++++

class CrossoverStrategy
{
public:
    virtual std::vector<Chromosome*> crossover(Chromosome *parent1, Chromosome *parent2) = 0;
    const std::vector<double> &getGenes(Chromosome *instance) { return instance != NULL ? instance->m_genes : std::vector<double>(); }; // OPTION #1 ... BOO! UGLY!
};

class CrossoverStrategyExample1; // OPTION #2 ... BOO! UGLY!

class Chromosome
{
public:
    // Friends
    friend class CrossoverStrategy;
    friend class CrossoverStrategyExample1; // OPTION #2 ... BOO! UGLY!
private:
    std::vector<double> m_genes;
};

// CrossoverStrategies.h ++++++++++++++++++++++++++++++++++++++++++++++++

#include "Chromosome.h"

class CrossoverStrategyExample1 : public CrossoverStrategy
{
public:
    virtual std::vector<Chromosome*> crossover(Chromosome *parent1, Chromosome *parent2);
private:
};

// CrossoverStrategies.cpp ++++++++++++++++++++++++++++++++++++++++++++++++

#include "CrossoverStrategies.h"

std::vector<Chromosome*> CrossoverStrategyExample1::crossover(Chromosome *parent1, Chromosome *parent2)
{
    // Do something with Chromosome private members
    // PROBLEM ... m_genes not accessible to subclasses? BOO BOO BOO!
    (for unsigned long i = 0; i < parent1->m_genes.size(); i++)
        parent1->m_genes[i] = 0.0;
}

【问题讨论】:

  • 不仅丑陋,而且致命。您不应返回对局部变量的引用。
  • 鲍勃是你的朋友。你把你的秘密托付给他。但是鲍勃的儿子格里夫(Griff)的毒品交易垃圾袋呢?你会因为信任 Bob 而自动信任 Griff 吗?没有。如果派生自 friends 的类自动成为 friends,那么您可以将任何您想要的子类化并完全覆盖封装。不是一个好计划。
  • 如果您不可能为指针传递空指针,请考虑通过引用而不是指针传递。几乎消除了整个家庭的意外搞砸。
  • 鉴于您希望多个类/函数能够访问构成 Chromosome 的数据,为什么不是数据 public
  • @Peter 因为那时 任何人 都可以访问私人数据。如果只有朋友(及其子类)可以访问,我会更喜欢。这是一个不错的选择 3,但风险是我们完全解开 Chromosome。

标签: c++ inheritance design-patterns friend


【解决方案1】:

选项 2 应该被拒绝,因为它不能扩展。您将不断修改 Chromosome 以使其与新的 CrossoverStrategies 保持同步。

选项 1 是一个奇怪的想法,因为它将Chromosome 的数据成员的getter 函数放在Chromosome 之外。如果getGenes 受到保护,我可以看到一些情况下这是一个有吸引力的想法,但我不相信这里。改为考虑

选项 1-A

class Chromosome
{
public:
    const std::vector<double>& getGenes() const
    {
        return m_genes;
    }
private:
    std::vector<double> m_genes;
};

每个可以访问Chromosome 的人都可以访问getGenes,但他们不能做任何事情来破坏它,Chromosome 仍然对它的用户一无所知。

选项 3:使用The Pimpl Idiom

简短而愚蠢的示例,带有一些缺陷以保持演示简短

染色体.h ++++++++++++++++++++++++++++++++++++++++++++++ ++++

#include <vector>
class Chromosome; // forward declaration only
class CrossoverStrategy
{
public:
    virtual ~CrossoverStrategy() = default;
    virtual std::vector<Chromosome*> crossover(Chromosome *parent1, Chromosome *parent2) = 0;
};

Chromosome * ChromosomeFactory(/* some construction parameters here */);
// should also provide a declaration of a factory function to provide CrossoverStrategies

CrossoverStrategies.h ++++++++++++++++++++++++++++++++++++++++++++++ ++++

#include "Chromosome.h"

class CrossoverStrategyExample1 : public CrossoverStrategy
{
public:
    virtual std::vector<Chromosome*> crossover(Chromosome *parent1, Chromosome *parent2);
private:
};

CrossoverStrategies.cpp ++++++++++++++++++++++++++++++++++++++++++++++ ++++

#include "CrossoverStrategies.h"
class Chromosome
{
public:
    std::vector<double> m_genes;

    // silence a warning
    Chromosome(): m_genes{}
    {

    }
};

// Because Chromosome is only defined in this file, only this file can use the internals 
// of Chromosome. They are public, but the outside world doesn't know that 

Chromosome * ChromosomeFactory(/* some construction parameters here */)
{
    // Probably makes and returns a pointer to a Chromosome,
    // but could pull it from a list, copy construct from a template, etc...
    return new Chromosome(/* some construction parameters here */);
}

// should also provide a definition of a factory function to provide CrossoverStrategies


std::vector<Chromosome*> CrossoverStrategyExample1::crossover(Chromosome *parent1,
                                                              Chromosome *parent2)
{
    for (unsigned long i = 0; i < parent1->m_genes.size(); i++)
        parent1->m_genes[i] = 0.0;
    return std::vector<Chromosome*>{}; // silence a warning
}

Main.cpp ++++++++++++++++++++++++++++++++++++++++++++++ ++++

#include "Chromosome.h"
#include "CrossoverStrategies.h" // A bad idea. Forces recompilation when strategies are added

int main()
{
    Chromosome * p1 = ChromosomeFactory(/* some construction parameters here */);
    p1->m_genes.push_back(0.0); // will fail to compile (incomplete type)
    Chromosome * p2 = ChromosomeFactory(/* some construction parameters here */);
    
    // probably should hide the next line with a factory as well
    CrossoverStrategy * strategy = new CrossoverStrategyExample1();
    strategy->crossover(p1, p2);
}

关于安全性的快速后记。它总是有代价的。通常它会使事情更难使用。它使攻击者更难,但也使合法用户更难。值不值就看你自己了。

【讨论】:

  • 感谢您的建议。我知道当我添加新策略时,我需要不断修改 Chromosome.h。但这是一个已知的限制,并且(仅)需要更改 2 行代码,因此对于代码的简洁性来说,这似乎是一个可以接受的折衷方案。
  • 关于您的选项 1 建议:整个目的是提供对 Chromosome 私有成员的非常量访问。如果我们将 getter 设为非 const,那么任何随机类都可以使用 Chromsome 的私有数据。显然,我只想将此访问限制为 CrossoverStrategy 及其子类。请记住,实际上我们正在使用最初的 Chromosome::crossover() 并将其外部化,以便我们可以使用不同的策略进行交换。 JavaScript 用匿名函数很好地做到了这一点,但相比之下我发现 C++ lamdbas 丑陋且不可读。
  • @cridgit 非CrossoverStrategies 可以用Chromsome 做什么?如果答案是没有,看看是否让Chromsomepublic 和应用the pimpl idiom 有意义。
  • 该代码包含一个需要对 Chromosome privates 执行的操作的示例:“parent1->m_genes[i] = 0.0;”它需要访问和修改此成员以及可能的其他私有成员。我查看了 pimpl,但这似乎也完全隐藏了私人成员,所以我不确定如何使用 pimpl 达到预期的结果,但感谢您的建议。
  • 我已经根据 Pimpl 模式使用上面的示例弄清楚了如何做到这一点,这正是我想要的。谢谢。我在头文件中使用了公共 ChromosomeImpl 变量。然后我在源文件中公开了 ChromosomeImpl 成员,因此只有 CrossoverStrategy 子类可以访问它们。效果很好!
【解决方案2】:

第一个显而易见的选项是考虑Chromosome 的成员是否应该是public。鉴于您希望任意数量的类能够访问其数据,一个明显的选择是将该数据设为public

第二个选项是Chromosome 为受影响的数据提供公共 getter 和 setter,例如;

 class Chromosome
 {
      public:

          std::vector<double> getGenes() const {return m_genes;};
          bool setGenes(const std::vector<double> &newgenes)
          {
               bool is_error = true;
               if (IsValid(newgnes))
               {
                    is_error = false;
                    m_genes = newgenes;
               }
               return is_error;    //  return true if change is rejected
          };

       private:

           std::vector<double> m_genes;
 };

然后所有CrossOverStrategy 及其派生类都需要做,给定指向Chromosomes 的有效指针,请求基因,做任何需要的事情,并且(完成后)将一组新基因提供给选定的@ 987654328@.

Chromosome 的封装通过各种措施得以保留,因为改变基因的唯一方法是通过Chromosome 的成员函数,即无法改变Chromosome 类控制之外的染色体中的基因。这允许Chromosome 进行它喜欢的任何检查,并在需要时拒绝坏基因。

没有必要让任何其他类或函数成为Chromosome 的朋友。一个关键优势是,只要从CrossOverStrategy 派生新类,就不必更改Chromosome 类。

权衡是通过复制完整集来检索和更改基因(复制的潜在性能损失)。但它通过直接或间接地向任何其他类提供对其私有成员的引用,避免了破坏Chromosome 类的封装。

如果复制完整的染色体集是一件坏事,请计算Chromosome 的一些附加成员函数,允许调用者请求部分更改(例如更新特定基因,将一组基因插入到指定位置)基因载体等)。这些附加功能需要遵循相同的原则:Chromosome 内的所有基因变化都通过Chromosome 的成员函数进行,并且没有“后门”机制让其他代码偷偷通过。

如果你真的想要,你可以将 Chromosome 的 setter 和 getter 设为 private 成员,并且只将基类 CrossOverStrategy 设为 friend。那么CrossOverStrategy 需要做的就是提供protected 只调用Chromosome 的私有助手的助手。

class CrossoverStrategy
{
    public:
       virtual std::vector<Chromosome*> crossover(Chromosome *parent1, Chromosome *parent2) = 0;

    protected:

        std::vector<double> getGenes(Chromosome *instance)
        {
           return instance ? instance->getGenes() : std::vector<double>();
        };

        bool setGenes(Chromosome *instance, const std::vector<double> &newgenes)
        {
           return instance ? instance->setGenes(newgenes)) : true;  // true indicates error
        };
};

这样,只有派生自CrossOverStrategy 的类才能访问protected 助手。如果Chromosome 的工作方式发生变化,那么在这种情况下唯一需要调整的类是基类CrossOverStrategy——因为它的派生类根本不(直接)访问Chromosome

【讨论】:

  • 彼得感谢您的可靠回答,这似乎是最“正确”的方式,当然不像我建议的 2 个选项那么难看。在接受这个作为答案之前,我会等待看看是否还有其他绝妙的想法。
【解决方案3】:

你的想法存在根本缺陷。

一方面,你说你不希望任何人能够弄乱基因载体。

另一方面,您希望CrossoverStrategy任何后代 能够弄乱基因向量。

但是有一个矛盾。类的“任何后代”“只是任何人”。任何用户都可以从任何类继承并使用您的基因载体做他们想做的事。他们只需要经历一次从CrossoverStrategy继承的小麻烦。

这就是为什么 C++ 中的友谊不能被继承的原因。如果是这样,访问控制在朋友类存在的情况下将毫无用处。

当然,您可以通过在CrossoverStrategy 中使用受保护的吸气剂来模拟可继承的友谊,正如答案之一所暗示的那样。但是这样做违背了访问控制的目的。它使基因阵列像公开的一样好。

【讨论】:

  • 虽然我同意除非在特定情况下,应避免公开私有变量,但封装不是二进制而是频谱。能够通过继承朋友来访问成员变量比 100% 公开访问要好一步。所以我不同意它完全违背了访问控制的目的——这个概念除了公开一切之外还有一些优点。
  • @cridgit 我看不到光谱。访问对所有人开放或关闭。没有半开或几乎关闭。
猜你喜欢
  • 1970-01-01
  • 2023-03-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-10
  • 2021-08-16
  • 1970-01-01
  • 2021-12-30
相关资源
最近更新 更多