Làm cách nào để loại bỏ trùng lặp mã giữa các hàm thành viên const và non-const tương tự?


242

Giả sử tôi có những điều sau đây class Xkhi tôi muốn trả lại quyền truy cập cho một thành viên nội bộ:

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    Z& Z(size_t index)
    {
        // massive amounts of code for validating index

        Z& ret = vecZ[index];

        // even more code for determining that the Z instance
        // at index is *exactly* the right sort of Z (a process
        // which involves calculating leap years in which
        // religious holidays fall on Tuesdays for
        // the next thousand years or so)

        return ret;
    }
    const Z& Z(size_t index) const
    {
        // identical to non-const X::Z(), except printed in
        // a lighter shade of gray since
        // we're running low on toner by this point
    }
};

Hai hàm thành viên X::Z()X::Z() constcó mã giống hệt nhau trong dấu ngoặc nhọn. Đây là mã trùng lặp và có thể gây ra sự cố bảo trì cho các hàm dài với logic phức tạp .

Có cách nào để tránh sự trùng lặp mã này không?


Trong ví dụ này tôi sẽ trả về một giá trị trong trường hợp const để bạn không thể tái cấu trúc bên dưới. int Z () const {return z; }
Matt Giá

1
Đối với các loại cơ bản, bạn hoàn toàn chính xác! Ví dụ đầu tiên của tôi không tốt lắm. Thay vào đó, hãy nói rằng thay vào đó chúng ta sẽ trả lại một số thể hiện của lớp. (Tôi đã cập nhật câu hỏi để phản ánh điều này.)
Kevin

Câu trả lời:


189

Để được giải thích chi tiết, vui lòng xem tiêu đề "Tránh trùng lặp trong chức năng constvà không phải constthành viên" trên trang. 23, trong Mục 3 "Sử dụng constbất cứ khi nào có thể", trong C ++ hiệu quả , biên tập 3d của Scott Meyers, ISBN-13: YAM321334879.

văn bản thay thế

Đây là giải pháp của Meyers (đơn giản hóa):

struct C {
  const char & get() const {
    return c;
  }
  char & get() {
    return const_cast<char &>(static_cast<const C &>(*this).get());
  }
  char c;
};

Hai phôi và gọi hàm có thể xấu nhưng đúng. Meyers có một lời giải thích kỹ lưỡng tại sao.


45
Không ai từng bị sa thải vì theo dõi Scott Meyers :-)
Steve Jessop

11
witkamp là chính xác rằng nói chung thật tệ khi sử dụng const_cast. Đây là một trường hợp cụ thể không như vậy, như Meyers giải thích. @Adam: ROM => const vẫn ổn. const == ROM rõ ràng là vô nghĩa vì bất kỳ ai cũng có thể bỏ non-const thành const willy-nilly: nó tương đương với việc chỉ chọn không sửa đổi một cái gì đó.
Steve Jessop

44
Nói chung, tôi khuyên bạn nên sử dụng const_cast thay vì static_cast để thêm const vì nó vô tình ngăn bạn thay đổi loại.
Greg Rogers

6
@Hellooodbye: Tôi nghĩ Meyers thừa nhận một chút thông minh từ người thiết kế giao diện lớp. Nếu get()consttrả về một cái gì đó được định nghĩa là một đối tượng const, thì không nên có một phiên bản không phải là const get(). Trên thực tế, suy nghĩ của tôi về điều này đã thay đổi theo thời gian: giải pháp mẫu là cách duy nhất để tránh trùng lặp kiểm tra tính chính xác của trình biên dịch, vì vậy cá nhân tôi sẽ không còn sử dụng const_castđể tránh sao chép mã, tôi chọn giữa việc đặt mã mã bị lừa vào một mẫu hàm hoặc nếu không thì nó bị lừa.
Steve Jessop

7
Hai mẫu sau đây giúp ích rất nhiều cho khả năng đọc của giải pháp này: template<typename T> const T& constant(T& _) { return const_cast<const T&>(_); }template<typename T> T& variable(const T& _) { return const_cast<T&>(_); }. Sau đó, bạn có thể làm:return variable(constant(*this).get());
Casey Rodarmor

64

Có, có thể tránh sự trùng lặp mã. Bạn cần sử dụng hàm const thành viên để có logic và có hàm không phải thành viên const gọi hàm const thành viên và truyền lại giá trị trả về cho một tham chiếu không const (hoặc con trỏ nếu các hàm trả về một con trỏ):

class X
{
   std::vector<Z> vecZ;

public:
   const Z& z(size_t index) const
   {
      // same really-really-really long access 
      // and checking code as in OP
      // ...
      return vecZ[index];
   }

   Z& z(size_t index)
   {
      // One line. One ugly, ugly line - but just one line!
      return const_cast<Z&>( static_cast<const X&>(*this).z(index) );
   }

 #if 0 // A slightly less-ugly version
   Z& Z(size_t index)
   {
      // Two lines -- one cast. This is slightly less ugly but takes an extra line.
      const X& constMe = *this;
      return const_cast<Z&>( constMe.z(index) );
   }
 #endif
};

LƯU Ý: Điều quan trọng là bạn KHÔNG đặt logic vào hàm non-const và có hàm const gọi hàm non-const - nó có thể dẫn đến hành vi không xác định. Lý do là một thể hiện của lớp hằng được chọn là một thể hiện không hằng. Hàm không phải thành viên có thể vô tình sửa đổi lớp, trạng thái tiêu chuẩn C ++ sẽ dẫn đến hành vi không xác định.


3
Wow ... thật kinh khủng. Bạn chỉ cần tăng số lượng mã, giảm độ rõ ràng và thêm hai stinkin 'const_cast <> s. Có lẽ bạn có một ví dụ trong tâm trí nơi điều này thực sự có ý nghĩa?
Shog9

14
Này đừng làm điều này!, Nó có thể xấu, nhưng theo Scott Meyers, đó là (gần như) cách chính xác. Xem C ++ hiệu quả , 3d ed, Mục 3 dưới tiêu đề "Tránh trùng lặp trong các hàm thành viên và không phải chi phí.
jwfearn

17
Trong khi tôi hiểu rằng giải pháp có thể xấu, hãy tưởng tượng rằng mã xác định những gì sẽ trả về dài 50 dòng. Sau đó, sao chép là rất không mong muốn - đặc biệt là khi bạn phải xác định lại mã. Tôi đã gặp điều này nhiều lần trong sự nghiệp của tôi.
Kevin

8
Sự khác biệt giữa cái này và Meyers là Meyers có static_cast <const X &> (* this). const_cast là để loại bỏ const, không thêm nó.
Steve Jessop

8
@VioletGiraffe chúng tôi biết rằng đối tượng ban đầu không được tạo const, vì nó không phải là thành viên của một đối tượng không const, mà chúng tôi biết vì chúng tôi đang ở trong một phương thức không phải là đối tượng nói. Trình biên dịch không thực hiện suy luận này, nó tuân theo một quy tắc bảo thủ. Tại sao bạn nghĩ const_cast tồn tại, nếu không phải là loại tình huống này?
Caleth 15/03/2017

47

C ++ 17 đã cập nhật câu trả lời tốt nhất cho câu hỏi này:

T const & f() const {
    return something_complicated();
}
T & f() {
    return const_cast<T &>(std::as_const(*this).f());
}

Điều này có những lợi thế mà nó:

  • Rõ ràng là những gì đang xảy ra
  • Có chi phí mã tối thiểu - nó phù hợp trong một dòng
  • Rất khó để hiểu sai (chỉ có thể bỏ đi một volatilecách tình cờ, nhưng volatilelà một vòng loại hiếm)

Nếu bạn muốn đi theo con đường khấu trừ đầy đủ thì điều đó có thể được thực hiện bằng cách có chức năng trợ giúp

template<typename T>
constexpr T & as_mutable(T const & value) noexcept {
    return const_cast<T &>(value);
}
template<typename T>
constexpr T * as_mutable(T const * value) noexcept {
    return const_cast<T *>(value);
}
template<typename T>
constexpr T * as_mutable(T * value) noexcept {
    return value;
}
template<typename T>
void as_mutable(T const &&) = delete;

Bây giờ bạn thậm chí không thể gây rối volatile, và việc sử dụng trông giống như

decltype(auto) f() const {
    return something_complicated();
}
decltype(auto) f() {
    return as_mutable(std::as_const(*this).f());
}

Lưu ý rằng "as_mutable" với quá tải const rvalue đã bị xóa (thường được ưu tiên hơn) ngăn ví dụ cuối hoạt động nếu f()trả về Tthay vì T&.
Max Truxa

1
@MaxTruxa: Vâng, và đây là một điều tốt. Nếu nó chỉ được biên dịch, chúng tôi sẽ có một tài liệu tham khảo lơ lửng. Trong trường hợp f()trả về T, chúng tôi không muốn có hai lần quá tải, constchỉ riêng phiên bản là đủ.
David Stone

Rất đúng, tôi xin lỗi vì cái rắm não đầy đủ của tôi ngày hôm qua, không biết tôi đã nghĩ gì khi viết bình luận đó. Tôi đã nhìn vào một cặp getter const / mutable trở lại a shared_ptr. Vì vậy, những gì tôi thực sự cần là một cái gì as_mutable_ptrđó trông gần giống như as_mutableở trên, ngoại trừ việc nó lấy và trả về shared_ptrvà sử dụng std::const_pointer_castthay vì const_cast.
Max Truxa

1
Nếu một phương thức trả về T const*thì điều này sẽ liên kết với T const* const&&chứ không phải ràng buộc với T const* const&(ít nhất là trong thử nghiệm của tôi, nó đã làm). Tôi đã phải thêm một quá tải cho T const*kiểu đối số cho các phương thức trả về một con trỏ.
khỉ0506

2
@ khỉ0506: Tôi đã cập nhật câu trả lời của mình để hỗ trợ con trỏ cũng như tham chiếu
David Stone

34

Tôi nghĩ rằng giải pháp của Scott Meyers có thể được cải thiện trong C ++ 11 bằng cách sử dụng chức năng trợ giúp tạm thời. Điều này làm cho ý định rõ ràng hơn nhiều và có thể được sử dụng lại cho nhiều người khác.

template <typename T>
struct NonConst {typedef T type;};
template <typename T>
struct NonConst<T const> {typedef T type;}; //by value
template <typename T>
struct NonConst<T const&> {typedef T& type;}; //by reference
template <typename T>
struct NonConst<T const*> {typedef T* type;}; //by pointer
template <typename T>
struct NonConst<T const&&> {typedef T&& type;}; //by rvalue-reference

template<typename TConstReturn, class TObj, typename... TArgs>
typename NonConst<TConstReturn>::type likeConstVersion(
   TObj const* obj,
   TConstReturn (TObj::* memFun)(TArgs...) const,
   TArgs&&... args) {
      return const_cast<typename NonConst<TConstReturn>::type>(
         (obj->*memFun)(std::forward<TArgs>(args)...));
}

Chức năng trợ giúp này có thể được sử dụng theo cách sau.

struct T {
   int arr[100];

   int const& getElement(size_t i) const{
      return arr[i];
   }

   int& getElement(size_t i) {
      return likeConstVersion(this, &T::getElement, i);
   }
};

Đối số đầu tiên luôn là con trỏ này. Thứ hai là con trỏ đến hàm thành viên để gọi. Sau đó, một lượng tùy ý của các đối số bổ sung có thể được chuyển qua để chúng có thể được chuyển tiếp đến hàm. Điều này cần C ++ 11 vì các mẫu matrixdic.


3
Thật xấu hổ khi chúng ta không phải std::remove_bottom_constđi cùng std::remove_const.
TBBle

Tôi không thích giải pháp này vì nó vẫn nhúng a const_cast. Bạn có thể tự tạo getElementmột mẫu và sử dụng đặc điểm của loại bên trong để mpl::conditionalloại bạn cần, như iterators hoặc constiterators nếu cần. Vấn đề thực sự là làm thế nào để tạo một phiên bản const của một phương thức khi phần này của chữ ký không thể được templatized?
v.oddou

2
@ v.oddou: std::remove_const<int const&>int const &(loại bỏ trình độ cấp cao nhất const), do đó, thể dục dụng cụ NonConst<T>trong câu trả lời này. Putative std::remove_bottom_constcó thể loại bỏ trình độ constchuyên môn cấp dưới và thực hiện chính xác những gì NonConst<T>hiện tại: std::remove_bottom_const<int const&>::type=> int&.
TBBle

4
Giải pháp này không hoạt động tốt, nếu getElementbị quá tải. Sau đó, con trỏ hàm không thể được giải quyết mà không đưa ra các tham số mẫu rõ ràng. Tại sao?
John

1
Bạn cần sửa câu trả lời để sử dụng chuyển tiếp hoàn hảo C ++ 11: likeConstVersion(TObj const* obj, TConstReturn (TObj::*memFun)(TArgs...) const, TArgs&&... args) { return const_cast<typename NonConst<TConstReturn>::type>((obj->*memFun)(std::forward<TArgs>(args)...)); }Hoàn thành: gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83
ShaulF

22

Một chút dài dòng hơn Meyers, nhưng tôi có thể làm điều này:

class X {

    private:

    // This method MUST NOT be called except from boilerplate accessors.
    Z &_getZ(size_t index) const {
        return something;
    }

    // boilerplate accessors
    public:
    Z &getZ(size_t index)             { return _getZ(index); }
    const Z &getZ(size_t index) const { return _getZ(index); }
};

Phương thức riêng tư có thuộc tính không mong muốn là nó trả về một Z không phải là const & cho một cá thể const, đó là lý do tại sao nó là riêng tư. Các phương thức riêng tư có thể phá vỡ các bất biến của giao diện bên ngoài (trong trường hợp này là bất biến mong muốn là "một đối tượng const không thể được sửa đổi thông qua các tham chiếu có được thông qua nó cho các đối tượng mà nó có-a").

Lưu ý rằng các nhận xét là một phần của mẫu - Giao diện của _getZ chỉ định rằng không bao giờ hợp lệ để gọi nó (ngoài các bộ truy cập, rõ ràng): dù sao cũng không có lợi ích nào để làm như vậy, bởi vì đó là thêm 1 ký tự để gõ và sẽ không dẫn đến mã nhỏ hơn hoặc nhanh hơn. Gọi phương thức này tương đương với việc gọi một trong những người truy cập bằng const_cast và bạn cũng không muốn làm điều đó. Nếu bạn lo lắng về việc làm cho lỗi rõ ràng (và đó là một mục tiêu công bằng), thì hãy gọi nó là const_cast_getZ thay vì _getZ.

Nhân tiện, tôi đánh giá cao giải pháp của Meyers. Tôi không phản đối triết học về nó. Tuy nhiên, cá nhân tôi thích một chút lặp lại có kiểm soát và một phương thức riêng chỉ phải được gọi trong một số trường hợp được kiểm soát chặt chẽ, qua một phương thức trông giống như nhiễu đường truyền. Chọn chất độc của bạn và gắn bó với nó.

[Chỉnh sửa: Kevin đã chỉ ra một cách đúng đắn rằng _getZ có thể muốn gọi một phương thức tiếp theo (giả sử GenerZ) được cấu thành theo cách tương tự như getZ. Trong trường hợp này, _getZ sẽ thấy const Z & và phải const_cast nó trước khi quay lại. Điều đó vẫn an toàn, vì bộ truy cập nồi hơi điều khiển mọi thứ, nhưng rõ ràng là nó không an toàn. Hơn nữa, nếu bạn làm điều đó và sau đó thay đổi GenerZ để luôn trả về const, thì bạn cũng cần thay đổi getZ để luôn trả về const, nhưng trình biên dịch sẽ không cho bạn biết rằng bạn làm.

Điểm sau đó về trình biên dịch cũng đúng với mẫu được đề xuất của Meyers, nhưng điểm đầu tiên về const_cast không rõ ràng thì không. Vì vậy, về sự cân bằng, tôi nghĩ rằng nếu _getZ hóa ra cần một const_cast cho giá trị trả về của nó, thì mẫu này sẽ mất rất nhiều giá trị của nó so với Meyers. Vì nó cũng chịu những bất lợi so với Meyers, tôi nghĩ rằng tôi sẽ chuyển sang anh ấy trong tình huống đó. Tái cấu trúc từ cái này sang cái khác thật dễ dàng - nó không ảnh hưởng đến bất kỳ mã hợp lệ nào khác trong lớp, vì chỉ có mã không hợp lệ và các lệnh gọi soạn sẵn _getZ.]


3
Điều này vẫn có vấn đề là thứ bạn trả về có thể là hằng số cho một thể hiện không đổi của X. Trong trường hợp đó, bạn vẫn yêu cầu const_cast trong _getZ (...). Nếu các nhà phát triển sau này sử dụng sai, nó vẫn có thể dẫn đến UB. Nếu thứ đang được trả lại là 'có thể thay đổi', thì đây là một giải pháp tốt.
Kevin

1
Bất kỳ chức năng riêng tư nào (quái, công khai cũng vậy) đều có thể bị các nhà phát triển sau này sử dụng sai, nếu họ chọn bỏ qua các hướng dẫn VỐN VỐN về việc sử dụng hợp lệ của nó, trong tệp tiêu đề và cả trong Doxygen, v.v. Tôi không thể dừng điều đó, và tôi không coi đó là vấn đề của mình vì hướng dẫn rất dễ hiểu.
Steve Jessop

13
-1: Điều này không hoạt động trong nhiều tình huống. Điều gì nếu somethingtrong _getZ()hàm là một biến thể? Trình biên dịch (hoặc ít nhất là một số trình biên dịch) sẽ phàn nàn rằng vì _getZ()là const, nên bất kỳ biến đối tượng nào được tham chiếu bên trong cũng là const. Vì vậy, somethingsau đó sẽ là const (nó sẽ là loại const Z&) và không thể được chuyển đổi thành Z&. Theo kinh nghiệm của tôi (thừa nhận có phần hạn chế), hầu hết thời gian somethinglà một biến đối tượng trong các trường hợp như thế này.
Trọng lực

2
@GravityBringer: thì "cái gì đó" cần liên quan đến a const_cast. Nó được dự định là một trình giữ chỗ cho mã được yêu cầu để có được một trả về không phải là const từ đối tượng const, không phải là một trình giữ chỗ cho những gì sẽ có trong getter trùng lặp. Vì vậy, "một cái gì đó" không chỉ là một biến thể hiện.
Steve Jessop

2
Tôi hiểu rồi. Điều đó thực sự làm giảm tính hữu dụng của kỹ thuật, mặc dù. Tôi sẽ xóa downvote, nhưng SO sẽ không cho tôi.
Trọng lực

22

Câu hỏi hay và câu trả lời hay. Tôi có một giải pháp khác, đó là không sử dụng phôi:

class X {

private:

    std::vector<Z> v;

    template<typename InstanceType>
    static auto get(InstanceType& instance, std::size_t i) -> decltype(instance.get(i)) {
        // massive amounts of code for validating index
        // the instance variable has to be used to access class members
        return instance.v[i];
    }

public:

    const Z& get(std::size_t i) const {
        return get(*this, i);
    }

    Z& get(std::size_t i) {
        return get(*this, i);
    }

};

Tuy nhiên, nó có sự xấu xí khi yêu cầu một thành viên tĩnh và nhu cầu sử dụng instancebiến bên trong nó.

Tôi đã không xem xét tất cả các tác động có thể (tiêu cực) của giải pháp này. Xin vui lòng cho tôi biết nếu có.


4
Vâng, hãy đi với thực tế đơn giản là bạn đã thêm nồi hơi. Nếu bất cứ điều gì, điều này nên được sử dụng như một ví dụ về lý do tại sao ngôn ngữ cần một cách để sửa đổi các vòng loại chức năng cùng với kiểu trả về auto get(std::size_t i) -> auto(const), auto(&&). Tại sao '&&'? À, vậy tôi có thể nói:auto foo() -> auto(const), auto(&&) = delete;
kfsone

gd1: chính xác những gì tôi đã nghĩ trong đầu. @kfsone và chính xác những gì tôi đã kết luận quá.
v.oddou

1
@kfsone cú pháp nên được kết hợp thistừ khóa. Tôi đề nghị template< typename T > auto myfunction(T this, t args) -> decltype(ident)Từ khóa này sẽ được công nhận là đối số đối tượng ẩn và để trình biên dịch nhận ra rằng hàm của tôi là thành viên hoặc T. Tsẽ được tự động suy luận trên trang web cuộc gọi, sẽ luôn là loại của lớp, nhưng với trình độ cv miễn phí.
v.oddou

2
Giải pháp đó cũng có lợi thế (so với giải pháp const_cast) để cho phép quay lại iteratorconst_iterator.
Jarod42

1
Nếu việc triển khai được di chuyển trong tệp cpp (và vì phương thức không trùng lặp không nên tầm thường, có lẽ đó là trường hợp), staticcó thể được thực hiện ở phạm vi tệp thay vì phạm vi lớp. :-)
Jarod42

8

Bạn cũng có thể giải quyết điều này với các mẫu. Giải pháp này hơi xấu (nhưng độ xấu được ẩn trong tệp .cpp) nhưng nó cung cấp trình biên dịch kiểm tra hằng số và không sao chép mã.

tập tin .h:

#include <vector>

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    const std::vector<Z>& GetVector() const { return vecZ; }
    std::vector<Z>& GetVector() { return vecZ; }

    Z& GetZ( size_t index );
    const Z& GetZ( size_t index ) const;
};

tập tin .cpp:

#include "constnonconst.h"

template< class ParentPtr, class Child >
Child& GetZImpl( ParentPtr parent, size_t index )
{
    // ... massive amounts of code ...

    // Note you may only use methods of X here that are
    // available in both const and non-const varieties.

    Child& ret = parent->GetVector()[index];

    // ... even more code ...

    return ret;
}

Z& X::GetZ( size_t index )
{
    return GetZImpl< X*, Z >( this, index );
}

const Z& X::GetZ( size_t index ) const
{
    return GetZImpl< const X*, const Z >( this, index );
}

Nhược điểm chính tôi có thể thấy là bởi vì tất cả việc triển khai phức tạp của phương thức đều nằm trong hàm toàn cục, nên bạn cần phải giữ các thành viên của X bằng các phương thức công khai như GetVector () ở trên (trong đó luôn cần phải có const và phiên bản không const) hoặc bạn có thể làm cho chức năng này trở thành một người bạn. Nhưng tôi không thích bạn bè.

[Chỉnh sửa: đã xóa không cần thiết bao gồm cstdio được thêm trong quá trình thử nghiệm.]


3
Bạn luôn có thể làm cho hàm triển khai phức tạp trở thành một thành viên tĩnh để có quyền truy cập vào các thành viên riêng. Hàm chỉ cần được khai báo trong tệp tiêu đề lớp, định nghĩa có thể nằm trong tệp thực hiện lớp. Rốt cuộc, nó là một phần của việc thực hiện lớp.
CB Bailey

Vâng, ý kiến ​​hay đấy! Tôi không thích các công cụ mẫu xuất hiện trong tiêu đề, nhưng nếu từ đây, nó có khả năng làm cho việc giả định khá đơn giản hơn rất nhiều thì có lẽ nó đáng giá.
Andy Balaam

+ 1 cho giải pháp này không trùng lặp bất kỳ mã nào, cũng không sử dụng bất kỳ mã xấu nào const_cast(có thể vô tình được sử dụng để chống lại thứ gì đó thực sự được coi là không phải là thứ không phải).
HelloGoodbye

Ngày nay, điều này có thể được đơn giản hóa với kiểu trả về được suy ra cho mẫu (đặc biệt hữu ích vì nó làm giảm những gì phải được sao chép trong lớp trong trường hợp thành viên).
Davis Herring

3

Làm thế nào về việc chuyển logic thành một phương thức riêng tư và chỉ thực hiện công cụ "lấy tham chiếu và trả về" bên trong getters? Trên thực tế, tôi sẽ khá bối rối về các hàm tĩnh và const bên trong một hàm getter đơn giản và tôi cho rằng nó xấu ngoại trừ các trường hợp cực kỳ hiếm!


Để tránh hành vi không xác định, bạn vẫn cần const_cast. Xem câu trả lời của Martin York và nhận xét của tôi ở đó.
Kevin

1
Kevin, câu trả lời của Martin York
Peter Nimmo

2

Có phải là gian lận để sử dụng bộ tiền xử lý?

struct A {

    #define GETTER_CORE_CODE       \
    /* line 1 of getter code */    \
    /* line 2 of getter code */    \
    /* .....etc............. */    \
    /* line n of getter code */       

    // ^ NOTE: line continuation char '\' on all lines but the last

   B& get() {
        GETTER_CORE_CODE
   }

   const B& get() const {
        GETTER_CORE_CODE
   }

   #undef GETTER_CORE_CODE

};

Nó không lạ mắt như các mẫu hoặc phôi, nhưng nó làm cho ý định của bạn ("hai chức năng này giống hệt nhau") khá rõ ràng.


1
Nhưng sau đó, bạn phải cẩn thận với dấu gạch chéo ngược (như thường lệ đối với các macro đa dòng) và ngoài ra, bạn mất phần tô sáng cú pháp trong hầu hết (nếu không phải tất cả) các trình soạn thảo.
Ruslan

2

Điều đáng ngạc nhiên với tôi là có rất nhiều câu trả lời khác nhau, nhưng hầu như tất cả đều dựa vào ma thuật mẫu nặng. Các mẫu rất mạnh mẽ, nhưng đôi khi các macro đánh bại chúng một cách đồng nhất. Tính linh hoạt tối đa thường đạt được bằng cách kết hợp cả hai.

Tôi đã viết một macro FROM_CONST_OVERLOAD()có thể được đặt trong hàm non-const để gọi hàm const.

Ví dụ sử dụng:

class MyClass
{
private:
    std::vector<std::string> data = {"str", "x"};

public:
    // Works for references
    const std::string& GetRef(std::size_t index) const
    {
        return data[index];
    }

    std::string& GetRef(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetRef(index) );
    }


    // Works for pointers
    const std::string* GetPtr(std::size_t index) const
    {
        return &data[index];
    }

    std::string* GetPtr(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetPtr(index) );
    }
};

Thực hiện đơn giản và có thể tái sử dụng:

template <typename T>
T& WithoutConst(const T& ref)
{
    return const_cast<T&>(ref);
}

template <typename T>
T* WithoutConst(const T* ptr)
{
    return const_cast<T*>(ptr);
}

template <typename T>
const T* WithConst(T* ptr)
{
    return ptr;
}

#define FROM_CONST_OVERLOAD(FunctionCall) \
  WithoutConst(WithConst(this)->FunctionCall)

Giải trình:

Như được đăng trong nhiều câu trả lời, mẫu điển hình để tránh trùng lặp mã trong hàm không phải là thành viên là:

return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );

Rất nhiều mẫu nồi hơi này có thể tránh được bằng cách sử dụng suy luận kiểu. Đầu tiên, const_castcó thể được gói gọn trong WithoutConst()đó, loại tham số kiểu đối số của nó và loại bỏ bộ định dạng. Thứ hai, một cách tiếp cận tương tự có thể được sử dụng WithConst()để xác định thiscon trỏ đủ điều kiện , cho phép gọi phương thức nạp chồng.

Phần còn lại là một macro đơn giản tiền tố cuộc gọi với đủ điều kiện chính xác this->và loại bỏ const khỏi kết quả. Do biểu thức được sử dụng trong macro hầu như luôn luôn là một hàm gọi đơn giản với các đối số được chuyển tiếp 1: 1, các nhược điểm của các macro như nhiều đánh giá không khởi động. Dấu chấm lửng và __VA_ARGS__cũng có thể được sử dụng, nhưng không cần thiết vì dấu phẩy (như phân tách đối số) xảy ra trong ngoặc đơn.

Cách tiếp cận này có một số lợi ích:

  • Cú pháp tối thiểu và tự nhiên - chỉ cần kết thúc cuộc gọi FROM_CONST_OVERLOAD( )
  • Không có chức năng thành viên bổ sung cần thiết
  • Tương thích với C ++ 98
  • Thực hiện đơn giản, không có siêu lập trình mẫu và không phụ thuộc
  • Extensible: Quan hệ const khác có thể được thêm vào (như const_iterator, std::shared_ptr<const T>, vv). Đối với điều này, chỉ đơn giản là quá tải WithoutConst()cho các loại tương ứng.

Hạn chế: giải pháp này được tối ưu hóa cho các tình huống trong đó tình trạng quá tải không phải là hoạt động chính xác giống như quá tải const, để các đối số có thể được chuyển tiếp 1: 1. Nếu logic của bạn khác và bạn không gọi phiên bản const qua this->Method(args), bạn có thể xem xét các phương pháp khác.


2

Dành cho những người (như tôi)

  • sử dụng c ++ 17
  • muốn thêm số lượng nồi hơi / lặp lại ít nhất
  • đừng bận tâm sử dụng macro (trong khi chờ đợi các siêu lớp ...),

đây là một mất:

#include <utility>
#include <type_traits>

template <typename T> struct NonConst;
template <typename T> struct NonConst<T const&> {using type = T&;};
template <typename T> struct NonConst<T const*> {using type = T*;};

#define NON_CONST(func)                                                     \
    template <typename... T> auto func(T&&... a)                            \
        -> typename NonConst<decltype(func(std::forward<T>(a)...))>::type   \
    {                                                                       \
        return const_cast<decltype(func(std::forward<T>(a)...))>(           \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

Về cơ bản, đây là sự kết hợp của các câu trả lời từ @Pait, @DavidStone và @ sh1 ( EDIT : và một cải tiến từ @cdhowie). Những gì nó thêm vào bảng là bạn nhận được chỉ với một dòng mã bổ sung mà chỉ cần đặt tên hàm (nhưng không có đối số hoặc trả về kiểu trùng lặp):

class X
{
    const Z& get(size_t index) const { ... }
    NON_CONST(get)
};

Lưu ý: gcc không biên dịch được điều này trước 8.1, clang-5 trở lên cũng như MSVC-19 đều hài lòng (theo trình thám hiểm trình biên dịch ).


Điều này chỉ làm việc thẳng lên cho tôi. Đây là một câu trả lời tuyệt vời, cảm ơn bạn!
Ngắn

Không phải họ decltype()cũng nên sử dụng std::forwardcác đối số để đảm bảo rằng chúng tôi đang sử dụng loại trả về đúng trong trường hợp chúng tôi có quá tải get()các loại tham chiếu khác nhau?
cdhowie

@cdhowie Bạn có thể cung cấp một ví dụ?
axxel

@axxel Nó được coi là địa ngục, nhưng ở đây bạn đi . Các NON_CONSTsuy luận vĩ mô kiểu trả về sai và const_casts để loại sai do sự thiếu chuyển tiếp trong các decltype(func(a...))loại. Thay thế chúng bằng decltype(func(std::forward<T>(a)...)) giải quyết điều này . (Chỉ có lỗi liên kết vì tôi chưa bao giờ xác định bất kỳ X::gettình trạng quá tải nào được khai báo .)
cdhowie

1
Cảm ơn @cdhowie, tôi đã lấy ví dụ của bạn để thực sự sử dụng quá tải không phải là const: coliru.stacked-crooking.com/a/0cedc7f4e789479e
axxel

1

Đây là phiên bản C ++ 17 của chức năng trợ giúp tĩnh mẫu, với và kiểm tra SFINAE tùy chọn.

#include <type_traits>

#define REQUIRES(...)         class = std::enable_if_t<(__VA_ARGS__)>
#define REQUIRES_CV_OF(A,B)   REQUIRES( std::is_same_v< std::remove_cv_t< A >, B > )

class Foobar {
private:
    int something;

    template<class FOOBAR, REQUIRES_CV_OF(FOOBAR, Foobar)>
    static auto& _getSomething(FOOBAR& self, int index) {
        // big, non-trivial chunk of code...
        return self.something;
    }

public:
    auto& getSomething(int index)       { return _getSomething(*this, index); }
    auto& getSomething(int index) const { return _getSomething(*this, index); }
};

Phiên bản đầy đủ: https://godbolt.org/z/mMK4r3


1

Tôi đã đưa ra một macro tạo các cặp hàm const / non-const tự động.

class A
{
    int x;    
  public:
    MAYBE_CONST(
        CV int &GetX() CV {return x;}
        CV int &GetY() CV {return y;}
    )

    //   Equivalent to:
    // int &GetX() {return x;}
    // int &GetY() {return y;}
    // const int &GetX() const {return x;}
    // const int &GetY() const {return y;}
};

Xem phần cuối của câu trả lời cho việc thực hiện.

Đối số của MAYBE_CONSTlà trùng lặp. Trong bản sao đầu tiên, CVđược thay thế bằng không có gì; và trong bản sao thứ hai, nó được thay thế bằng const.

Không có giới hạn về số lần CVcó thể xuất hiện trong đối số macro.

Có một chút bất tiện. Nếu CVxuất hiện bên trong dấu ngoặc đơn, cặp dấu ngoặc đơn này phải được bắt đầu bằng CV_IN:

// Doesn't work
MAYBE_CONST( CV int &foo(CV int &); )

// Works, expands to
//         int &foo(      int &);
//   const int &foo(const int &);
MAYBE_CONST( CV int &foo CV_IN(CV int &); )

Thực hiện:

#define MAYBE_CONST(...) IMPL_CV_maybe_const( (IMPL_CV_null,__VA_ARGS__)() )
#define CV )(IMPL_CV_identity,
#define CV_IN(...) )(IMPL_CV_p_open,)(IMPL_CV_null,__VA_ARGS__)(IMPL_CV_p_close,)(IMPL_CV_null,

#define IMPL_CV_null(...)
#define IMPL_CV_identity(...) __VA_ARGS__
#define IMPL_CV_p_open(...) (
#define IMPL_CV_p_close(...) )

#define IMPL_CV_maybe_const(seq) IMPL_CV_a seq IMPL_CV_const_a seq

#define IMPL_CV_body(cv, m, ...) m(cv) __VA_ARGS__

#define IMPL_CV_a(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_b)
#define IMPL_CV_b(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_a)

#define IMPL_CV_const_a(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_b)
#define IMPL_CV_const_b(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_a)

Triển khai Pre-C ++ 20 không hỗ trợ CV_IN:

#define MAYBE_CONST(...) IMPL_MC( ((__VA_ARGS__)) )
#define CV ))((

#define IMPL_MC(seq) \
    IMPL_MC_end(IMPL_MC_a seq) \
    IMPL_MC_end(IMPL_MC_const_0 seq)

#define IMPL_MC_identity(...) __VA_ARGS__
#define IMPL_MC_end(...) IMPL_MC_end_(__VA_ARGS__)
#define IMPL_MC_end_(...) __VA_ARGS__##_end

#define IMPL_MC_a(elem) IMPL_MC_identity elem IMPL_MC_b
#define IMPL_MC_b(elem) IMPL_MC_identity elem IMPL_MC_a
#define IMPL_MC_a_end
#define IMPL_MC_b_end

#define IMPL_MC_const_0(elem)       IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a(elem) const IMPL_MC_identity elem IMPL_MC_const_b
#define IMPL_MC_const_b(elem) const IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a_end
#define IMPL_MC_const_b_end

0

Thông thường, các hàm thành viên mà bạn cần các phiên bản const và non-const là getters và setters. Hầu hết thời gian chúng là một lớp lót nên việc sao chép mã không phải là vấn đề.


2
Điều đó có thể đúng hầu hết thời gian. Nhưng có những trường hợp ngoại lệ.
Kevin

1
getters dù sao, một setter const không có ý nghĩa nhiều;)
jwfearn

Tôi có nghĩa là getter non-const thực sự là một setter. :)
Dima

0

Tôi đã làm điều này cho một người bạn đã biện minh chính đáng cho việc sử dụng const_cast... không biết về nó Tôi có thể đã làm một cái gì đó như thế này (không thực sự thanh lịch):

#include <iostream>

class MyClass
{

public:

    int getI()
    {
        std::cout << "non-const getter" << std::endl;
        return privateGetI<MyClass, int>(*this);
    }

    const int getI() const
    {
        std::cout << "const getter" << std::endl;
        return privateGetI<const MyClass, const int>(*this);
    }

private:

    template <class C, typename T>
    static T privateGetI(C c)
    {
        //do my stuff
        return c._i;
    }

    int _i;
};

int main()
{
    const MyClass myConstClass = MyClass();
    myConstClass.getI();

    MyClass myNonConstClass;
    myNonConstClass.getI();

    return 0;
}

0

Tôi muốn đề xuất một mẫu hàm tĩnh trợ giúp riêng, như thế này:

class X
{
    std::vector<Z> vecZ;

    // ReturnType is explicitly 'Z&' or 'const Z&'
    // ThisType is deduced to be 'X' or 'const X'
    template <typename ReturnType, typename ThisType>
    static ReturnType Z_impl(ThisType& self, size_t index)
    {
        // massive amounts of code for validating index
        ReturnType ret = self.vecZ[index];
        // even more code for determining, blah, blah...
        return ret;
    }

public:
    Z& Z(size_t index)
    {
        return Z_impl<Z&>(*this, index);
    }
    const Z& Z(size_t index) const
    {
        return Z_impl<const Z&>(*this, index);
    }
};

-1

Bài viết DDJ này cho thấy một cách sử dụng chuyên môn hóa mẫu mà không yêu cầu bạn sử dụng const_cast. Đối với một chức năng đơn giản như vậy, nó thực sự không cần thiết mặc dù.

boost :: any_cast (tại một thời điểm, nó không còn nữa) sử dụng const_cast từ phiên bản const gọi phiên bản không phải const để tránh trùng lặp. Mặc dù vậy, bạn không thể áp đặt ngữ nghĩa const cho phiên bản không phải const vì vậy bạn phải rất cẩn thận với điều đó.

Cuối cùng, một số sao chép mã ổn miễn là hai đoạn mã trực tiếp chồng lên nhau.


Bài viết DDJ dường như đề cập đến các trình vòng lặp - không liên quan đến câu hỏi. Các trình lặp không phải là dữ liệu không đổi - chúng là các trình vòng lặp trỏ đến dữ liệu không đổi.
Kevin

-1

Để thêm vào giải pháp jwfearn và kevin cung cấp, đây là giải pháp tương ứng khi hàm trả về shared_ptr:

struct C {
  shared_ptr<const char> get() const {
    return c;
  }
  shared_ptr<char> get() {
    return const_pointer_cast<char>(static_cast<const C &>(*this).get());
  }
  shared_ptr<char> c;
};

-1

Không tìm thấy những gì tôi đang tìm kiếm, vì vậy tôi đã lăn một vài ...

Cái này hơi dài dòng, nhưng có ưu điểm là xử lý nhiều phương thức quá tải cùng tên (và kiểu trả về) cùng một lúc:

struct C {
  int x[10];

  int const* getp() const { return x; }
  int const* getp(int i) const { return &x[i]; }
  int const* getp(int* p) const { return &x[*p]; }

  int const& getr() const { return x[0]; }
  int const& getr(int i) const { return x[i]; }
  int const& getr(int* p) const { return x[*p]; }

  template<typename... Ts>
  auto* getp(Ts... args) {
    auto const* p = this;
    return const_cast<int*>(p->getp(args...));
  }

  template<typename... Ts>
  auto& getr(Ts... args) {
    auto const* p = this;
    return const_cast<int&>(p->getr(args...));
  }
};

Nếu bạn chỉ có một constphương thức cho mỗi tên, nhưng vẫn còn nhiều phương thức để sao chép, thì bạn có thể thích phương thức này:

  template<typename T, typename... Ts>
  auto* pwrap(T const* (C::*f)(Ts...) const, Ts... args) {
    return const_cast<T*>((this->*f)(args...));
  }

  int* getp_i(int i) { return pwrap(&C::getp_i, i); }
  int* getp_p(int* p) { return pwrap(&C::getp_p, p); }

Thật không may, điều này bị hỏng ngay khi bạn bắt đầu nạp chồng tên (danh sách đối số của đối số con trỏ hàm dường như chưa được giải quyết tại thời điểm đó, vì vậy nó không thể tìm thấy kết quả khớp cho đối số hàm). Mặc dù bạn cũng có thể tạo mẫu theo cách đó:

  template<typename... Ts>
  auto* getp(Ts... args) { return pwrap<int, Ts...>(&C::getp, args...); }

Nhưng các đối số tham chiếu đến constphương thức không khớp với các đối số có giá trị rõ ràng với mẫu và nó bị phá vỡ. Không chắc chắn lý do tại sao. Đây là lý do tại sao .

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.