非成员非朋友函数语法

时间:2008-12-01 22:26:56

标签: c++ class-design

他们是否可以使用与成员函数相同的“点”表示法在对象上使用非成员非友元函数?

我可以从一个类中拉出(任何)成员,并让用户以与他们一直相同的方式使用它吗?

更长的解释:

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如何为mem_fun提供*此参数)。

7 个答案:

答案 0 :(得分: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 ;
}
从审美角度来看,这比功能性符号要好得多。

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

编辑:一些例子

假设无论出于什么原因,我想创建两个“类似整数”的类。 第一个是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提升,如上所示),它不适用于成员方法。

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

    作为奖励:如果你想要一个运算符(或非运算符函数)添加到没有运算符的对象(例如,&lt; windows.h&gt;的GUID结构),那么你可以,没有需要修改结构本身。对于运算符来说,语法是自然的,对于非运算符,语法很好......

    免责声明:当然这些类很愚蠢:set / getValue几乎可以直接访问其内部。但是用Herb Sutter在Monoliths "Unstrung"中提出的字符串替换整数,你会看到一个更真实的案例。

答案 1 :(得分:1)

无法使用点表示法编写非成员非朋友,即因为“运算符”。不能超载。

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

答案 2 :(得分:1)

  

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

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

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

请注意其他语言选择其他方法;这导致在C#和VB中引入了“扩展方法”,它允许模拟静态函数的方法调用语法(即正是你想到的)。然后,C#和VB是严格的面向对象语言,因此对方法调用使用单一符号可能更为重要。

除此之外,函数总是属于命名空间 - 虽然我自己偶尔会违反这个规则(但只在一个编译单元中,即我相当于main.cpp,这里没有'发挥作用)。

答案 3 :(得分: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)而无需关心类型所在的命名空间(取决于参数类型,编译器会计算出命名空间本身),并且无需关心正在发生的事情在幕后。考虑使用beginend作为自由函数。这正是boost::range does的内容。

答案 4 :(得分:1)

可以使用单一语法,但也许不是你喜欢的语法。不要在类范围内放置一个insert(),而是将其作为类的朋友。现在你可以写

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

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

future中,我们也可以编写这样的多态函数,所以这可能是C ++的方式,而不是人为地试图将自由函数强制为点语法。

答案 5 :(得分: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-y。

答案 6 :(得分:0)

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