Chiến lược DR C ++ DRY


14

Để tránh trùng lặp liên quan đến C ++ không tầm thường, có trường hợp const_cast sẽ hoạt động nhưng chức năng const riêng không trả về không const?

Trong mục 3 C ++ hiệu quả của Scott Meyers , ông gợi ý rằng const_cast kết hợp với một diễn viên tĩnh có thể là một cách hiệu quả và an toàn để tránh mã trùng lặp, ví dụ:

const void* Bar::bar(int i) const
{
  ...
  return variableResultingFromNonTrivialDotDotDotCode;
}
void* Bar::bar(int i)
{
  return const_cast<void*>(static_cast<const Bar*>(this)->bar(i));
}

Meyers tiếp tục giải thích rằng có chức năng const gọi hàm non-const là nguy hiểm.

Mã dưới đây là một ví dụ phản ánh hiển thị:

  • trái với đề xuất của Meyers, đôi khi const_cast kết hợp với dàn diễn viên tĩnh lại nguy hiểm
  • đôi khi có chức năng const gọi non-const ít nguy hiểm hơn
  • đôi khi cả hai cách sử dụng const_cast đều ẩn các lỗi trình biên dịch hữu ích
  • tránh một const_cast và có thêm một thành viên riêng tư trả về một không phải const là một lựa chọn khác

Các chiến lược const_cast của việc tránh sao chép mã có được coi là thông lệ tốt không? Bạn có muốn chiến lược phương pháp riêng thay thế? Có trường hợp const_cast sẽ hoạt động nhưng một phương thức riêng tư sẽ không? Có những lựa chọn khác (ngoài việc sao chép)?

Mối quan tâm của tôi với các chiến lược const_cast là ngay cả khi mã chính xác khi được viết, sau này trong quá trình bảo trì, mã có thể trở nên không chính xác và const_cast sẽ ẩn một lỗi trình biên dịch hữu ích. Có vẻ như một chức năng riêng tư phổ biến thường an toàn hơn.

class Foo
{
  public:
    Foo(const LongLived& constLongLived, LongLived& mutableLongLived)
    : mConstLongLived(constLongLived), mMutableLongLived(mutableLongLived)
    {}

    // case A: we shouldn't ever be allowed to return a non-const reference to something we only have a const reference to

    // const_cast prevents a useful compiler error
    const LongLived& GetA1() const { return mConstLongLived; }
    LongLived& GetA1()
    {
      return const_cast<LongLived&>( static_cast<const Foo*>(this)->GetA1() );
    }

    /* gives useful compiler error
    LongLived& GetA2()
    {
      return mConstLongLived; // error: invalid initialization of reference of type 'LongLived&' from expression of type 'const LongLived'
    }
    const LongLived& GetA2() const { return const_cast<Foo*>(this)->GetA2(); }
    */

    // case B: imagine we are using the convention that const means thread-safe, and we would prefer to re-calculate than lock the cache, then GetB0 might be correct:

    int GetB0(int i) { return mCache.Nth(i); }
    int GetB0(int i) const { return Fibonachi().Nth(i); }

    /* gives useful compiler error
    int GetB1(int i) const { return mCache.Nth(i); } // error: passing 'const Fibonachi' as 'this' argument of 'int Fibonachi::Nth(int)' discards qualifiers
    int GetB1(int i)
    {
      return static_cast<const Foo*>(this)->GetB1(i);
    }*/

    // const_cast prevents a useful compiler error
    int GetB2(int i) { return mCache.Nth(i); }
    int GetB2(int i) const { return const_cast<Foo*>(this)->GetB2(i); }

    // case C: calling a private const member that returns non-const seems like generally the way to go

    LongLived& GetC1() { return GetC1Private(); }
    const LongLived& GetC1() const { return GetC1Private(); }

  private:
    LongLived& GetC1Private() const { /* pretend a bunch of lines of code instead of just returning a single variable*/ return mMutableLongLived; }

    const LongLived& mConstLongLived;
    LongLived& mMutableLongLived;
    Fibonachi mCache;
};

class Fibonachi
{ 
    public:
      Fibonachi()
      {
        mCache.push_back(0);
        mCache.push_back(1);
      }

      int Nth(int n) 
      {
        for (int i=mCache.size(); i <= n; ++i)
        {
            mCache.push_back(mCache[i-1] + mCache[i-2]);
        }
        return mCache[n];
      }

      int Nth(int n) const
      {
          return n < mCache.size() ? mCache[n] : -1;
      }
    private:
      std::vector<int> mCache;
};

class LongLived {};

Một getter chỉ trả về một thành viên ngắn hơn một thành viên mà gọi và gọi phiên bản khác của chính nó. Thủ thuật này có nghĩa là cho các chức năng phức tạp hơn trong đó mức tăng của sự trùng lặp lớn hơn các rủi ro của việc truyền.
Sebastian Redl

@SebastianRedl Tôi đồng ý rằng sao chép sẽ tốt hơn nếu chỉ trả lại thành viên. Vui lòng tưởng tượng rằng nó phức tạp hơn, ví dụ thay vì trả về mConstLongLiving, chúng ta có thể gọi một hàm trên mConstLongLiving trả về một tham chiếu const mà sau đó được sử dụng để gọi một hàm khác trả về tham chiếu const mà chúng ta không sở hữu và chỉ có quyền truy cập vào một phiên bản const của. Tôi hy vọng vấn đề rõ ràng là const_cast có thể xóa const khỏi thứ mà chúng ta không có quyền truy cập không phải là const.
JDiMatteo

4
Tất cả điều này có vẻ vô lý với các ví dụ đơn giản, nhưng trùng lặp liên quan đến const trong mã thực, lỗi trình biên dịch const rất hữu ích trong thực tế (thường để bắt lỗi sai) và tôi ngạc nhiên rằng giải pháp "C ++ hiệu quả" được đề xuất là lạ và dường như cặp phôi dễ bị lỗi. Một thành viên const tư nhân trả lại một phi-const dường như rõ ràng vượt trội so với dàn diễn viên đôi, và tôi muốn biết liệu tôi có thiếu thứ gì không.
JDiMatteo

Câu trả lời:


7

Khi triển khai các hàm const và non-const chỉ khác nhau bởi liệu ptr / tham chiếu được trả về có phải là const hay không, chiến lược DRY tốt nhất là:

  1. nếu viết một trình truy cập, hãy xem xét liệu bạn có thực sự cần người truy cập hay không, xem câu trả lời của cmasterhttp://c2.com/cgi/wiki?AccessorsAreEvil
  2. chỉ cần sao chép mã nếu nó không quan trọng (ví dụ: chỉ trả lại một thành viên)
  3. không bao giờ sử dụng const_cast để tránh trùng lặp const
  4. để tránh trùng lặp không tầm thường, hãy sử dụng hàm const riêng trả về một hằng số mà cả hàm const và hàm không công khai gọi

ví dụ

public:
  LongLived& GetC1() { return GetC1Private(); }
  const LongLived& GetC1() const { return GetC1Private(); }
private:
  LongLived& GetC1Private() const { /* non-trivial DRY logic here */ }

Chúng ta hãy gọi đây là hàm const riêng trả về mẫu không const .

Đây là chiến lược tốt nhất để tránh trùng lặp theo cách đơn giản trong khi vẫn cho phép trình biên dịch thực hiện kiểm tra hữu ích và báo cáo các thông báo lỗi liên quan.


lập luận của bạn khá thuyết phục, nhưng tôi khá bối rối làm thế nào bạn có thể có được một tham chiếu không liên quan đến một cái gì đó từ một conství dụ (trừ khi tham chiếu là một cái gì đó được khai báo mutable, hoặc trừ khi bạn sử dụng một const_castnhưng trong cả hai trường hợp không có vấn đề gì để bắt đầu ). Ngoài ra, tôi không thể tìm thấy bất cứ điều gì trên "hàm const riêng trả về mẫu không phải const" (nếu nó được dự định là một trò đùa để gọi nó là mẫu .... nó không vui;)
idclev 463035818

1
Dưới đây là một ví dụ biên dịch dựa trên mã trong câu hỏi: ideone.com/wBE1GB . Xin lỗi, tôi không có ý nói nó như một trò đùa, nhưng tôi đã có ý đặt tên cho nó ở đây (trong trường hợp không chắc là nó xứng đáng với tên đó), và tôi đã cập nhật từ ngữ trong câu trả lời để cố gắng làm rõ hơn. Đã vài năm kể từ khi tôi viết bài này và tôi không nhớ tại sao tôi nghĩ một ví dụ chuyển tham chiếu trong hàm tạo có liên quan.
JDiMatteo

Cảm ơn ví dụ, tôi không có thời gian, nhưng tôi chắc chắn sẽ quay lại với nó. Fwiw ở đây là một câu trả lời trình bày cách tiếp cận tương tự và trong các ý kiến, các vấn đề tương tự đã được chỉ ra: stackoverflow.com/a/124209/4117728
idclev 463035818

1

Đúng, bạn đã đúng: Nhiều chương trình C ++ cố gắng không chính xác là vi phạm nghiêm trọng nguyên tắc DRY và ngay cả thành viên tư nhân trở lại không phải là quá phức tạp để có được sự thoải mái.

Tuy nhiên, bạn bỏ lỡ một quan sát: sao chép mã do tính chính xác chỉ là vấn đề nếu bạn cấp quyền truy cập mã khác cho các thành viên dữ liệu của mình. Điều này trong bản thân nó là vi phạm đóng gói. Nói chung, loại sao chép mã này xảy ra chủ yếu ở những người truy cập đơn giản (xét cho cùng, bạn đang trao quyền truy cập cho các thành viên hiện có, giá trị trả về thường không phải là kết quả của phép tính).

Kinh nghiệm của tôi là trừu tượng tốt không có xu hướng bao gồm người truy cập. Do đó, tôi phần lớn tránh vấn đề này bằng cách xác định các hàm thành viên thực sự làm gì đó, thay vì chỉ cung cấp quyền truy cập cho các thành viên dữ liệu; Tôi cố gắng mô hình hóa hành vi thay vì dữ liệu. Mục đích chính của tôi trong việc này là thực sự có được một sự trừu tượng hóa từ cả các lớp của tôi và các hàm thành viên riêng lẻ của chúng, thay vì chỉ sử dụng các đối tượng của tôi làm các thùng chứa dữ liệu. Nhưng phong cách này cũng khá thành công trong việc tránh hàng tấn bộ truy cập một dòng const / non-const lặp đi lặp lại rất phổ biến trong hầu hết các mã.


Có vẻ như tranh luận về việc liệu người truy cập có tốt hay không, ví dụ như xem cuộc thảo luận tại c2.com/cgi/wiki?AccessorsAreEvil . Trong thực tế, bất kể bạn nghĩ gì về người truy cập, các cơ sở mã lớn thường sử dụng chúng và nếu họ sử dụng chúng, sẽ tốt hơn nếu tuân thủ nguyên tắc DRY. Vì vậy, tôi nghĩ rằng câu hỏi xứng đáng có nhiều câu trả lời hơn là bạn không nên hỏi nó.
JDiMatteo 16/07/2015

1
Đó chắc chắn là một câu hỏi đáng để hỏi :-) Và tôi thậm chí sẽ không phủ nhận rằng thỉnh thoảng bạn cần người truy cập. Tôi chỉ nói rằng một phong cách lập trình không dựa trên những người truy cập sẽ giảm đáng kể vấn đề. Nó không giải quyết vấn đề hoàn toàn, nhưng ít nhất nó cũng đủ tốt cho tôi.
cmaster - phục hồi monica
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.