5

他们是一种使用与成员函数相同的“点”表示法在对象上使用非成员非朋友函数的方法吗?

我可以将(任何)成员从课程中拉出来,并让用户以他们一贯的方式使用它吗?

更长的解释:

Scott Meyers、Herb Sutter 等人认为,非成员非友元函数是对象接口的一部分,并且可以改进封装。我同意他们的观点。

然而,在最近阅读了这篇文章之后:http ://www.gotw.ca/gotw/084.htm我发现自己质疑语法含义。

在那篇文章中,Herb 建议拥有一个inserterasereplace成员,以及几个同名的非成员非朋友函数。

这是否意味着,正如我所认为的那样,Herb 认为某些函数应该与点符号一起使用,而其他函数应该作为全局函数使用?

std::string s("foobar");

s.insert( ... ); /* One like this */
insert( s , ...); /* Others like this */

编辑:

感谢大家提供非常有用的答案,但是,我认为我的问题的重点被忽略了。

我特别没有提到运算符的具体情况,以及它们如何保留“自然”符号。您也不应该将所有内容包装在名称空间中。这些东西写在我链接的文章中。

问题本身是:

在文章中,Herb 建议一个 insert() 方法是成员,而其余的是非成员非友元函数。

这意味着要使用一种形式的 insert(),您必须使用点符号,而对于其他形式,您不需要。

只是我,还是听起来很疯狂?

我有一种预感,也许您可​​以使用单一语法。(我在想 Boost::function 如何将 *this 参数用于 mem_fun)。

4

7 回答 7

4

是的,这意味着对象的部分接口由非成员函数组成。

对于 T 类的对象,它涉及使用以下表示法这一事实是正确的:

void T::doSomething(int value) ;     // method
void doSomething(T & t, int value) ; // non-member non-friend function

如果你想要 doSomething 函数/方法返回 void,并有一个名为“value”的 int 参数。

但有两件事值得一提。

首先是一个类的接口的函数部分应该在同一个命名空间中。这是使用命名空间的另一个原因(如果需要另一个原因),如果只是为了“组合”一个对象和作为其接口一部分的函数。

好的部分是它促进了良好的封装。但不好的部分是它使用了类似函数的符号,我个人非常不喜欢。

二是运营商不受此限制。例如,类 T 的 += 运算符可以写成两种方式:

T & operator += (T & lhs, const T & rhs) ;
{
   // do something like lhs.value += rhs.value
   return lhs ;
}

T & T::operator += (const T & rhs) ;
{
   // do something like this->value += rhs.value
   return *this ;
}

但是这两种符号都被用作:

void doSomething(T & a, T & b)
{
   a += b ;
}

从美学的角度来看,这比类似函数的符号要好得多。

现在,能够从同一个接口编写一个函数,并且仍然能够通过“.”调用它,这将是一个非常酷的语法糖。表示法,就像在 C# 中一样,正如 michalmocny 所提到的。

编辑:一些例子

假设无论出于何种原因,我都想创建两个“类似整数”的类。第一个是 IntegerMethod:

class IntegerMethod
{
   public :
      IntegerMethod(const int p_iValue) : m_iValue(p_iValue) {}
      int getValue() const { return this->m_iValue ; }
      void setValue(const int p_iValue) { this->m_iValue = p_iValue ; }

      IntegerMethod & operator += (const IntegerMethod & rhs)
      {
         this->m_iValue += rhs.getValue() ;
         return *this ;
      }

      IntegerMethod operator + (const IntegerMethod & rhs) const
      {
         return IntegerMethod (this->m_iValue + rhs.getValue()) ;
      }

      std::string toString() const
      {
         std::stringstream oStr ;
         oStr << this->m_iValue ;
         return oStr.str() ;
      }

   private :
      int m_iValue ;
} ;

这个类有 6 个方法可以访问它的内部。

第二个是 IntegerFunction:

class IntegerFunction
{
   public :
      IntegerFunction(const int p_iValue) : m_iValue(p_iValue) {}
      int getValue() const { return this->m_iValue ; }
      void setValue(const int p_iValue) { this->m_iValue = p_iValue ; }

   private :
      int m_iValue ;
} ;

IntegerFunction & operator += (IntegerFunction & lhs, const IntegerFunction & rhs)
{
   lhs.setValue(lhs.getValue() + rhs.getValue()) ;
   return lhs ;
}

IntegerFunction operator + (const IntegerFunction & lhs, const IntegerFunction & rhs)
{
   return IntegerFunction(lhs.getValue() + rhs.getValue()) ;
}

std::string toString(const IntegerFunction & p_oInteger)
{
   std::stringstream oStr ;
   oStr << p_oInteger.getValue() ;
   return oStr.str() ;
}

它只有 3 种方法,因此减少了可以访问其内部的代码数量。它有3个非会员非好友功能。

这两个类可以用作:

void doSomething()
{
   {
      IntegerMethod iMethod(25) ;
      iMethod += 35 ;
      std::cout << "iMethod   : " << iMethod.toString() << std::endl ;

      IntegerMethod result(0), lhs(10), rhs(20) ;
      result = lhs + 20 ;
      // result = 10 + rhs ; // WON'T COMPILE
      result = 10 + 20 ;
      result = lhs + rhs ;
   }

   {
      IntegerFunction iFunction(125) ;
      iFunction += 135 ;
      std::cout << "iFunction : " << toString(iFunction) << std::endl ;

      IntegerFunction result(0), lhs(10), rhs(20) ;
      result = lhs + 20 ;
      result = 10 + rhs ;
      result = 10 + 20 ;
      result = lhs + rhs ;
   }
}

当我们比较运算符的使用(“+”和“+=”)时,我们看到使运算符成为成员或非成员在其表面用途上没有区别。尽管如此,还是有两个区别:

  1. 该成员可以访问其所有内部。非成员必须使用公共成员方法

  2. 对于一些二元运算符,如 +、*,进行类型提升很有趣,因为在一种情况下(即,lhs 提升,如上所示),它不适用于成员方法。

现在,如果我们比较非运算符使用(“toString”),我们会发现对于类 Java 开发人员来说,成员非运算符使用比非成员函数更“自然”。尽管不熟悉,但对于 C++ 来说,重要的是要接受这一点,尽管它有语法,但从 OOP 的角度来看,非成员版本更好,因为它无法访问类内部。

作为奖励:如果您想将一个操作符(或非操作符函数)添加到一个没有操作符的对象(例如,<windows.h> 的 GUID 结构),那么您无需修改结构本身。对于运算符,语法将是自然的,对于非运算符,嗯......

免责声明:这些类当然是愚蠢的:set/getValue 几乎可以直接访问其内部。但是,正如 Herb Sutter 在Monoliths "Unstrung"中提出的那样,将 Integer 替换为字符串,您将看到更真实的案例。

于 2008-12-01T22:53:56.207 回答
1

没有办法用点符号来写非会员非朋友,即因为“操作员”。不能超载。

您应该始终将非成员非友元类包装在匿名命名空间(如果只有当前翻译单元需要这些函数)或一些对用户有意义的命名空间中。

于 2008-12-01T22:39:57.327 回答
1

然而,在最近阅读了这篇文章之后:http ://www.gotw.ca/gotw/084.htm我发现自己质疑语法含义。

语法含义在编写良好的 C++ 库中随处可见:C++ 到处使用自由函数。这对于具有 OOP 背景的人来说是不寻常的,但它是 C++ 中的最佳实践。例如,考虑 STL 标头<algorithm>

因此,使用点符号成为规则的例外,而不是相反。

注意其他语言选择其他方法;这导致在 C# 和 VB 中引入“扩展方法”,允许模拟静态函数的方法调用语法(即完全符合您的想法)。再说一次,C# 和 VB 是严格的面向对象的语言,因此对方法调用使用单一符号可能更重要。

除此之外,函数总是属于一个命名空间——尽管我自己偶尔会违反这条规则(但只在一个编译单元中,即我的等价于main.cpp,这不起作用)。

于 2008-12-01T23:08:26.350 回答
1

就个人而言,我喜欢自由功能的可扩展性。函数就是一个size很好的例子:

// joe writes this container class:
namespace mylib {
    class container { 
        // ... loads of stuff ...
    public:
        std::size_t size() const { 
            // do something and return
        }
    };

    std::size_t size(container const& c) {
        return c.size();
    } 
}

// another programmer decides to write another container...
namespace bar {
    class container {
        // again, lots of stuff...
    public:
        std::size_t size() const {
            // do something and return
        }
    };

    std::size_t size(container const& c) {
        return c.size();
    } 
}

// we want to get the size of arrays too
template<typename T, std::size_t n>
std::size_t size(T (&)[n]) {
    return n;
}

现在考虑使用自由大小函数的代码:

int main() {
    mylib::container c;
    std::size_t c_size = size(c);

    char data[] = "some string";
    std::size_t data_size = size(data);
}

如您所见,您可以直接使用size(object)而无需关心类型所在的命名空间(取决于参数类型,编译器会自行计算命名空间),也无需关心幕后发生的事情。考虑也使用 likebeginendas 自由函数。这也正是boost::range 如此

于 2008-12-02T01:47:46.887 回答
1

可以使用一种语法,但可能不是您喜欢的那种。与其在你的类范围内放置一个 insert(),不如让它成为你的类的朋友。现在你可以写

mystring s;
insert(s, "hello");
insert(s, other_s.begin(), other_s.end());
insert(s, 10, '.');

对于任何非虚拟的、公共的方法,都等价于将其定义为非成员友元函数。如果混合点/无点语法困扰你,那么一定要让这些方法成为朋友函数。没有区别。

未来我们也可以写出这样的多态函数,所以也许这就是 C++ 的方式,而不是人为地试图将自由函数强制变成点语法

于 2008-12-02T08:31:12.970 回答
1

如果您想保留点表示法,但也想保留不需要成为类之外的朋友的单独函数(因此它们无法访问私有成员从而破坏封装),您可能会编写一个 mixin 类。要么使“常规”在mixin中插入纯虚拟,要么保持非虚拟并使用CRTP:

template<typename DERIVED, typename T>
struct OtherInsertFunctions {
    void insertUpsideDown(T t) {
        DERIVED *self = static_cast<DERIVED*>(this);
        self->insert(t.turnUpsideDown());
    }
    void insertBackToFront(T t) // etc.
    void insert(T t, Orientation o) // this one is tricksy because it's an
                                    // overload, so requires the 'using' declaration
};

template<typename T>
class MyCollection : public OtherInsertFunctions<MyCollection,T> {
public:
    // using declaration, to prevent hiding of base class overloads
    using OtherInsertFunctions<MyCollection,T>::insert;
    void insert(T t);
    // and the rest of the class goes here
};

反正就是这样。但正如其他人所说,C++ 程序员并不“应该”反对自由函数,因为你“应该”一直在寻找编写通用算法(如 std::sort)的方法,而不是添加成员函数特定的类。让所有东西都成为一种方法更符合 Java 风格。

于 2008-12-02T14:50:57.353 回答
0

是的,它们应该是全局或命名空间范围的。非成员非友元函数在 C# 中看起来更漂亮,它们确实使用点表示法(它们被称为扩展方法)。

于 2008-12-01T22:34:04.517 回答