【问题标题】:Using non-member non-friend functions instead of member functions: disadvantages?使用非成员非朋友函数而不是成员函数:缺点?
【发布时间】:2020-12-16 06:58:12
【问题描述】:

Scott Meyers 长期以来一直提倡使用非成员非友元函数而不是成员函数来改进封装。我可以看到这样做的好处。

但是,在我看来,一个缺点是:

我有一些自定义图像类的自定义元数据类,其中包含许多数据成员。有几种格式可以保存图像,并且必须将元数据转换为这些格式可以采用的格式(ENVI、png、TIFF...)。现在我按照 Scott 的建议将这些转换函数放入单独的命名空间中。它们本质上使用公共接口将所有成员复制到适合最终元数据格式的内容中,但它们需要包含所有数据成员。 示例:

// file Metadata.h
class Metadata
{
   // Getters
     std::string GetDescription() const;
     std::string GetTimeStamp() const;
     float       GetExposureTimeInMilliSeconds() const;
  // Setters
  // ...
   private:
     std::string m_description;
     std::string m_timeStamp;
     float       m_exposureTimeInMilliSeconds;

   // Added later with associated getters/setters:
   // std::string m_location;
   // std::string m_nameOfPersonWhoTookThePicture;
};

// File UtilityFunctions.h
namespace UtilityFunctions
{
    ENVIMetadata ConvertMetadataToENVIMetadata(const Metadata &i_metadata)\
    {
        ENVIMetadata envi;
        envi.AddMetadata<string>("Description", GetDescription());
        envi.AddMetadata<string>("Time stamp", GetTimeStamp());
        envi.AddMetadata<float>("Exposure time", GetExposureTimeInMilliSeconds());
    }
}

我看到的问题是,当其他人从事该项目并且该人将另一个数据成员添加到元数据时,他/她需要记住将此数据成员添加到所有转换函数中。由于它们位于不同的 header/cpp 文件中,因此很容易忘记这一点,并且我们有一个不明显的错误,即并非所有数据成员都保存在元数据中。如果函数是公共成员,查看头文件(在添加新数据成员时)可能会提醒该人也在其中添加成员,那么完成的必要性只在该文件中。

要点是,使用公共接口确实可以保证(如果接口没有改变),如果类中的某些内容发生变化,基于它的函数将继续工作,但如果向公共接口添加附加功能,则不能保证完整性类,也需要添加到这些函数中。

在某些情况下人们会建议不要遵循此建议吗?对于这种特定情况,是否有一些范式可以两全其美?

【问题讨论】:

  • Meyer 的建议的形式是“如果一个函数可以作为成员或非成员实现,那么更喜欢非朋友非成员”。这与“始终使用非朋友非会员”不同。
  • 是的,这就是我隐含的意思。我的示例中的转换函数也可以是。

标签: c++ oop


【解决方案1】:

我不确定我是否一定同意非成员与成员函数的讨论,因为非成员当然不会改进封装。无论如何,我建议在 C++17 中使用 structured bindings 来帮助解决这个问题。

// file Metadata.h
struct Metadata
{
     std::string m_description;
     std::string m_timeStamp;
     float       m_exposureTimeInMilliSeconds;

   // Added later:
   // std::string m_location;
   // std::string m_nameOfPersonWhoTookThePicture;
};

// File UtilityFunctions.h
namespace UtilityFunctions
{
    ENVIMetadata ConvertMetadataToENVIMetadata(const Metadata &metadata)
    {
        const auto& [description, timestamp, exposureTimeInMilliSeconds] =
            metadata;
        ENVIMetadata envi;
        envi.AddMetadata<string>("Description", description);
        envi.AddMetadata<string>("Time stamp", timeStamp);
        envi.AddMetadata<float>("Exposure time", exposureTimeInMilliSeconds);
    }
}

稍后添加字段 m_locationm_nameOfPersonWhoTookThePicture 时,结构化绑定声明将产生错误,指出您没有提供足够的标识符。

【讨论】:

  • 这似乎是一个非常有用的建议!我不知道这一点,并且肯定会在某个时候使用它。也许迈耶斯对可维护性的担忧并不是很现实,因为似乎添加成员变量而不是删除/更改成员变量的可能性更大。在任何一种情况下,最好有一些东西坏了,因为在哪里修复东西很明显。可悲的是,我们公司的代码仍在 VS2015 中,需要将我们的内部库迁移到 VS2017(由于某种原因,并非所有内容都兼容?),所以我还不能使用 C++17。有没有其他解决方案?
  • 对不起,双重评论,但如果元数据结构以某种方式发生变化,例如 m_timeStamp 被删除并替换为例如包含当天天气描述的另一个字符串,功能现在有所不同,但结构化绑定不知道,一切都会毫无怨言地运行。这似乎也引入了自己的危险。我仍在寻找一种方法来使此类功能自动运行,或者在某些情况发生变化时使它们自动中断。
【解决方案2】:

您可以提供Metadata 的类似元组的视图,并让转换函数实例化std::index_sequence 以填充结果;

// file Metadata.h
class Metadata
{
   // Getters
     std::string GetDescription() const;
     std::string GetTimeStamp() const;
     float       GetExposureTimeInMilliSeconds() const;

     template<size_t I> static const char * name();

  // Setters
  // ...
   private:
     std::string m_description;
     std::string m_timeStamp;
     float       m_exposureTimeInMilliSeconds;

   // Added later with associated getters/setters:
   // std::string m_location;
   // std::string m_nameOfPersonWhoTookThePicture;
};

namespace std
{
    template<> class tuple_size<Metadata> : public std::integral_constant<std::size_t, 3> {}; // later 5

    template<> class tuple_element<0, Metadata>{ using type = std::string; };
    template<> class tuple_element<1, Metadata>{ using type = std::string; };
    template<> class tuple_element<2, Metadata>{ using type = float; };
    /* Later add
    template<> class tuple_element<3, Metadata>{ using type = std::string; };
    template<> class tuple_element<4, Metadata>{ using type = std::string; };
    */
}

template<size_t I> std::tuple_element_t<I, Metadata> get(const Metadata & meta);
template<> std::string get<0>(const Metadata & meta) { return meta.GetDescription(); }
template<> std::string get<1>(const Metadata & meta) { return meta.GetTimeStamp(); }
template<> float get<2>(const Metadata & meta) { return meta.GetExposureTimeInMilliSeconds(); }
/* Later add
template<> std::string get<3>(const Metadata & meta) { return meta.GetLocation(); }
template<> std::string get<4>(const Metadata & meta) { return meta.GetPhotographerName(); }
*/

template<> const char * Metadata::name<0>() { return "Description"; }
template<> const char * Metadata::name<1>() { return "Time Stamp"; }
template<> const char * Metadata::name<2>() { return "Exposure Time"; }
/* Later add
template<> const char * Metadata::name<3>() { return "Location"; }
template<> const char * Metadata::name<2>() { return "PhotographerName"; }
*/

添加成员时转换函数不会改变

// File UtilityFunctions.h
namespace UtilityFunctions
{
    namespace detail
    {
        template<size_t... Is>
        ENVIMetadata ConvertMetadataToENVIMetadata(const Metadata &i_metadata, std::index_sequence<Is...>)
        {
            ENVIMetadata envi;
            envi.AddMetadata<std::tuple_element_t<Is, Metadata>>(Metadata::name<Is>(), get<Is>(i_metadata))...;
            return envi;
        }
    }

    ENVIMetadata ConvertMetadataToENVIMetadata(const Metadata &i_metadata)\
    {
        return detail::ConvertMetadataToENVIMetadata(i_metadata, std::make_index_sequence<std::tuple_size_v<Metadata>>{})
    }
}

【讨论】: