Làm thế nào tôi có thể nhận được địa chỉ của một đối tượng một cách đáng tin cậy khi toán tử & bị quá tải?


170

Hãy xem xét chương trình sau:

struct ghost
{
    // ghosts like to pretend that they don't exist
    ghost* operator&() const volatile { return 0; }
};

int main()
{
    ghost clyde;
    ghost* clydes_address = &clyde; // darn; that's not clyde's address :'( 
}

Làm thế nào để tôi có được clydeđịa chỉ?

Tôi đang tìm kiếm một giải pháp sẽ hoạt động tốt như nhau cho tất cả các loại đối tượng. Một giải pháp C ++ 03 sẽ rất tuyệt, nhưng tôi cũng quan tâm đến các giải pháp C ++ 11. Nếu có thể, hãy tránh mọi hành vi cụ thể thực hiện.

Tôi biết std::addressofmẫu khuôn hàm của C ++ 11 , nhưng không quan tâm đến việc sử dụng nó ở đây: Tôi muốn hiểu cách người triển khai Thư viện Chuẩn có thể triển khai mẫu hàm này.


41
@jalf: Chiến lược đó có thể chấp nhận được, nhưng bây giờ tôi đã đấm vào đầu những cá nhân, làm thế nào để tôi làm việc xung quanh mã đáng ghê tởm của họ? :-)
James McNellis

5
@jalf Uhm, đôi khi bạn cần quá tải toán tử này và trả về một đối tượng proxy. Mặc dù tôi không thể nghĩ ra một ví dụ vừa nãy.
Konrad Rudolph

5
@Konrad: tôi cũng vậy. Nếu bạn cần điều đó, tôi đề nghị một lựa chọn tốt hơn có thể là xem xét lại thiết kế của bạn, vì quá tải toán tử đó chỉ gây ra quá nhiều vấn đề. :)
jalf

2
@Konrad: Trong khoảng 20 năm lập trình C ++, tôi đã từng cố gắng làm quá tải toán tử đó. Đó là vào lúc bắt đầu của hai mươi năm. Ồ, và tôi đã thất bại trong việc sử dụng nó. Do đó, mục nhập quá tải của nhà điều hành cho biết "Địa chỉ đơn vị của nhà điều hành không bao giờ bị quá tải." Bạn sẽ nhận được bia miễn phí vào lần tới khi chúng tôi gặp nếu bạn có thể đưa ra một ví dụ thuyết phục cho việc quá tải nhà điều hành này. (Tôi biết bạn sẽ rời Berlin, vì vậy tôi có thể cung cấp dịch vụ này một cách an toàn :))
sbi

5
CComPtr<>CComQIPtr<>bị quá tảioperator&
Simon Richter

Câu trả lời:


102

Cập nhật: trong C ++ 11, người ta có thể sử dụng std::addressofthay vì boost::addressof.


Trước tiên chúng ta hãy sao chép mã từ Boost, trừ đi trình biên dịch hoạt động xung quanh các bit:

template<class T>
struct addr_impl_ref
{
  T & v_;

  inline addr_impl_ref( T & v ): v_( v ) {}
  inline operator T& () const { return v_; }

private:
  addr_impl_ref & operator=(const addr_impl_ref &);
};

template<class T>
struct addressof_impl
{
  static inline T * f( T & v, long ) {
    return reinterpret_cast<T*>(
        &const_cast<char&>(reinterpret_cast<const volatile char &>(v)));
  }

  static inline T * f( T * v, int ) { return v; }
};

template<class T>
T * addressof( T & v ) {
  return addressof_impl<T>::f( addr_impl_ref<T>( v ), 0 );
}

Điều gì xảy ra nếu chúng ta vượt qua một tham chiếu đến chức năng ?

Lưu ý: addressofkhông thể được sử dụng với một con trỏ để hoạt động

Trong C ++ nếu void func();được khai báo, thì functham chiếu đến một hàm không có đối số và không trả về kết quả. Tham chiếu này đến một chức năng có thể được chuyển đổi một cách tầm thường thành một con trỏ thành chức năng - từ @Konstantin: Theo 13.3.3.2 cả hai T &T *không thể phân biệt cho các chức năng. Cái đầu tiên là một chuyển đổi Nhận dạng và cái thứ hai là chuyển đổi Hàm-Con trỏ cả hai đều có thứ hạng "Kết hợp chính xác" (13.3.3.1.1 bảng 9).

Các tham chiếu đến chức năng đi qua addr_impl_ref, có một sự nhập nhằng trong việc giải quyết tình trạng quá tải cho các lựa chọn f, được giải quyết nhờ vào lập luận giả 0, mà là một intđầu tiên và có thể được đề bạt lên một long(chuyển đổi Integral).

Vì vậy, chúng tôi chỉ đơn giản trả về con trỏ.

Điều gì xảy ra nếu chúng ta vượt qua một loại với toán tử chuyển đổi?

Nếu toán tử chuyển đổi mang lại một giá trị T*thì chúng ta có một sự mơ hồ: đối với f(T&,long)Khuyến mãi tích hợp là bắt buộc đối với đối số thứ hai trong khi đối f(T*,int)với toán tử chuyển đổi được gọi vào lần đầu tiên (nhờ @litb)

Đó là khi addr_impl_refbắt đầu. Tiêu chuẩn C ++ bắt buộc một chuỗi chuyển đổi có thể chứa tối đa một chuyển đổi do người dùng xác định. Bằng cách gói loại addr_impl_refvà buộc sử dụng trình tự chuyển đổi, chúng tôi "vô hiệu hóa" bất kỳ toán tử chuyển đổi nào mà loại đi kèm.

Do đó, f(T&,long)quá tải được chọn (và Khuyến mãi tích hợp được thực hiện).

Điều gì xảy ra cho bất kỳ loại khác?

Do đó, f(T&,long)quá tải được chọn, vì có loại không khớp với T*tham số.

Lưu ý: từ các nhận xét trong tệp liên quan đến khả năng tương thích Borland, các mảng không phân rã thành các con trỏ, nhưng được chuyển qua tham chiếu.

Điều gì xảy ra trong tình trạng quá tải này?

Chúng tôi muốn tránh áp dụng operator&cho các loại, vì nó có thể đã bị quá tải.

Tiêu chuẩn đảm bảo reinterpret_castcó thể được sử dụng cho công việc này (xem câu trả lời của @Matteo Italia: 5.2.10 / 10).

Boost thêm một số niceties với constvolatilevòng loại để tránh cảnh báo trình biên dịch (và sử dụng đúng cách const_castđể loại bỏ chúng).

  • Truyền T&tớichar const volatile&
  • Tách constvolatile
  • Áp dụng &toán tử để lấy địa chỉ
  • Quay trở lại một T*

Các const/ volatiletung hứng là một chút ma thuật đen, nhưng nó đơn giản hóa công việc (hơn là cung cấp 4 quá tải). Lưu ý rằng kể từ khi Tlà không đủ tiêu chuẩn, nếu chúng ta vượt qua một ghost const&, sau đó T*ghost const*, do đó vòng loại đã không thực sự bị mất.

EDIT: quá tải con trỏ được sử dụng cho con trỏ đến các chức năng, tôi đã sửa đổi phần giải thích ở trên. Tôi vẫn không hiểu tại sao nó lại cần thiết .

Đầu ra ideone sau đây tổng hợp điều này, phần nào.


2
"Điều gì xảy ra nếu chúng ta vượt qua một con trỏ?" một phần là không chính xác. Nếu chúng ta chuyển một con trỏ tới một số loại U, hàm addressof, loại 'T' được suy ra là 'U *' và addr_impl numf sẽ có hai lần quá tải: 'f (U * &, long)' và 'f (U **, int) ', rõ ràng cái đầu tiên sẽ được chọn.
Konstantin Oznobihin

@Konstantin: đúng, tôi đã nghĩ rằng hai ftình trạng quá tải trong đó các mẫu hàm, trong khi chúng là các hàm thành viên thông thường của một lớp mẫu, cảm ơn vì đã chỉ ra nó. (Bây giờ tôi chỉ cần tìm ra việc sử dụng quá tải là gì, mẹo nào?)
Matthieu M.

Đây là một câu trả lời tuyệt vời, được giải thích tốt. Tôi hình dung có nhiều thứ hơn thế chỉ là "bỏ qua char*". Cảm ơn bạn, Matthieu.
James McNellis

@James: Tôi đã nhận được rất nhiều sự giúp đỡ từ @Konstantin, người sẽ đánh vào đầu tôi bằng gậy bất cứ khi nào tôi mắc lỗi: D
Matthieu M.

3
Tại sao nó cần phải làm việc xung quanh các loại có chức năng chuyển đổi? Nó sẽ không thích kết hợp chính xác hơn việc gọi bất kỳ chức năng chuyển đổi T*nào? EDIT: Bây giờ tôi thấy. Nó sẽ, nhưng với 0lập luận, nó sẽ kết thúc trong một thập tự giá , vì vậy sẽ mơ hồ.
Julian Schaub - litb

99

Sử dụng std::addressof.

Bạn có thể nghĩ về nó như làm những điều sau hậu trường:

  1. Giải thích lại đối tượng như một tài liệu tham khảo
  2. Lấy địa chỉ đó (sẽ không gọi quá tải)
  3. Truyền con trỏ trở lại một con trỏ của loại của bạn.

Các triển khai hiện có (bao gồm Boost.Addressof) thực hiện chính xác điều đó, chỉ cần chăm sóc thêm constvolatileđủ điều kiện.


16
Tôi thích giải thích này tốt hơn so với lựa chọn trên vì nó có thể dễ hiểu.
Sled

49

Thủ thuật đằng sau boost::addressofvà việc thực hiện được cung cấp bởi @Luc Danton dựa vào sự kỳ diệu của reinterpret_cast; tiêu chuẩn nêu rõ tại §5.2.10 10

Một biểu thức giá trị của kiểu T1có thể được chuyển thành kiểu tham chiếu kiểu thành hình chữ nhật T2nếu một biểu thức của kiểu con trỏ kiểu thành thành con trỏ T1có thể được chuyển đổi rõ ràng thành kiểu con trỏ kiểu hình thành con trỏ T2bằng cách sử dụng a reinterpret_cast. Đó là, một tham chiếu cast reinterpret_cast<T&>(x)có tác dụng tương tự như chuyển đổi *reinterpret_cast<T*>(&x)với các toán tử tích hợp &*toán tử. Kết quả là một giá trị tham chiếu đến cùng một đối tượng như giá trị nguồn, nhưng với một loại khác.

Bây giờ, điều này cho phép chúng ta chuyển đổi một tham chiếu đối tượng tùy ý thành một char &(với trình độ cv nếu tham chiếu đủ điều kiện cv), bởi vì bất kỳ con trỏ nào cũng có thể được chuyển đổi thành (có thể đủ điều kiện cv) char *. Bây giờ chúng ta có một char &, toán tử quá tải trên đối tượng không còn phù hợp nữa và chúng ta có thể lấy địa chỉ với &toán tử dựng sẵn .

Việc triển khai tăng cường thêm một vài bước để làm việc với các đối tượng đủ điều kiện cv: bước đầu tiên reinterpret_castđược thực hiện const volatile char &, nếu không, một char &diễn viên đơn giản sẽ không hoạt động constvà / hoặc volatiletham chiếu ( reinterpret_castkhông thể xóa const). Sau đó, constvolatileđược xóa cùng với const_cast, địa chỉ được lấy &và cuối cùng reinterpet_castlà loại "chính xác" được thực hiện.

Điều const_castcần thiết là loại bỏ const/ volatilecó thể đã được thêm vào các tham chiếu không phải là hằng số / dễ bay hơi, nhưng nó không "làm hại" những gì const/ volatiletham chiếu ở vị trí đầu tiên, bởi vì trận chung kết reinterpret_castsẽ bổ sung lại tiêu chuẩn cv nếu nó là ở nơi đầu tiên ( reinterpret_castkhông thể loại bỏ constnhưng có thể thêm nó).

Đối với phần còn lại của mã trong addressof.hpp, có vẻ như hầu hết là dành cho cách giải quyết. Các static inline T * f( T * v, int )dường như là cần thiết chỉ dành cho các trình biên dịch Borland, nhưng sự hiện diện của nó giới thiệu sự cần thiết addr_impl_ref, nếu không loại con trỏ sẽ được đánh bắt bởi tình trạng quá tải thứ hai này.

Chỉnh sửa : các tình trạng quá tải khác nhau có chức năng khác nhau, xem @Matthieu M. câu trả lời tuyệt vời .

Chà, tôi cũng không còn chắc về điều này nữa; Tôi nên điều tra thêm về mã đó, nhưng bây giờ tôi đang nấu bữa tối :), tôi sẽ xem xét nó sau.


Matthieu M. giải thích về việc chuyển con trỏ đến địa chỉ là không chính xác. Đừng làm hỏng câu trả lời tuyệt vời của bạn bằng các chỉnh sửa như vậy :)
Konstantin Oznobihin

"ngon miệng", điều tra thêm cho thấy rằng quá tải được gọi để tham khảo các chức năng void func(); boost::addressof(func);. Tuy nhiên, việc loại bỏ quá tải không ngăn gcc 4.3.4 biên dịch mã và tạo ra cùng một đầu ra, vì vậy tôi vẫn không hiểu tại sao cần phải có quá tải này.
Matthieu M.

@Matthieu: Có vẻ là một lỗi trong gcc. Theo 13.3.3.2, cả T & và T * không thể phân biệt được các chức năng. Cái đầu tiên là một chuyển đổi Nhận dạng và cái thứ hai là chuyển đổi Hàm-Con trỏ cả hai đều có thứ hạng "Kết hợp chính xác" (13.3.3.1.1 bảng 9). Vì vậy, cần phải có thêm đối số.
Konstantin Oznobihin

@Matthieu: Chỉ cần thử nó với gcc 4.3.4 ( ideone.com/2f34P ) và có sự mơ hồ như mong đợi. Bạn đã thử quá tải các hàm thành viên như trong triển khai addressof hoặc các mẫu hàm miễn phí chưa? Cái thứ hai (như ideone.com/vjCRs ) sẽ dẫn đến tình trạng quá tải 'T *' được chọn do các quy tắc khấu trừ đối số tem tem (14.8.2.1/2).
Konstantin Oznobihin

2
@cquilguy: Tại sao bạn nghĩ nó nên? Tôi đã tham chiếu các phần tiêu chuẩn C ++ cụ thể quy định trình biên dịch nên làm gì và tất cả các trình biên dịch mà tôi có quyền truy cập (bao gồm nhưng không giới hạn ở gcc 4.3.4, comeau-online, VC6.0-VC2010) như tôi đã mô tả. Bạn có thể vui lòng giải thích lý do của bạn về trường hợp này?
Konstantin Oznobihin

11

Tôi đã thấy một triển khai thực hiện addressofđiều này:

char* start = &reinterpret_cast<char&>(clyde);
ghost* pointer_to_clyde = reinterpret_cast<ghost*>(start);

Đừng hỏi tôi làm thế nào để tuân thủ điều này!


5
Hợp pháp. char*là ngoại lệ được liệt kê để gõ quy tắc răng cưa.
Cún con

6
@DeadMG Tôi không nói điều này là không phù hợp. Tôi đang nói rằng bạn không nên hỏi tôi :)
Luc Danton

1
@DeadMG Không có vấn đề răng cưa ở đây. Câu hỏi là: được reinterpret_cast<char*>xác định rõ.
tò mò

2
@cquilguy và câu trả lời là có, nó luôn được phép truyền bất kỳ loại con trỏ nào [unsigned] char *và từ đó đọc biểu diễn đối tượng của đối tượng nhọn. Đây là một lĩnh vực khác, nơi charcó đặc quyền đặc biệt.
gạch dưới

@underscore_d Chỉ vì một diễn viên "luôn được phép" không có nghĩa là bạn có thể làm bất cứ điều gì với kết quả của dàn diễn viên.
tò mò

5

Hãy xem boost :: addressof và cách thực hiện.


1
Mã Boost, trong khi thú vị, không giải thích cách thức hoạt động của kỹ thuật của nó (cũng không giải thích tại sao cần có hai tình trạng quá tải).
James McNellis

bạn có nghĩa là quá tải 'tĩnh T * f (T * v, int)'? Có vẻ như nó chỉ cần cho cách giải quyết của Borland C. Cách tiếp cận được sử dụng ở đó là khá đơn giản. Điều duy nhất tinh tế (không chuẩn) là chuyển đổi 'T &' thành 'char &'. Mặc dù tiêu chuẩn, cho phép truyền từ 'T *' đến 'char *' dường như không có yêu cầu nào như vậy đối với việc truyền tham chiếu. Tuy nhiên, người ta có thể mong đợi nó hoạt động giống hệt nhau trên hầu hết các trình biên dịch.
Konstantin Oznobihin

@Konstantin: quá tải được sử dụng vì đối với một con trỏ, addressofsẽ trả về chính con trỏ đó. Người ta cho rằng đó là những gì người dùng muốn hay không, nhưng đó là cách nó được chỉ định.
Matthieu M.

@Matthieu: bạn có chắc không? Theo như tôi có thể nói, bất kỳ loại nào (bao gồm cả các loại con trỏ) được bọc bên trong một addr_impl_ref, do đó, quá tải con trỏ không bao giờ được gọi là ...
Matteo Italia

1
@KonstantinOznobihin điều này không thực sự trả lời câu hỏi, vì tất cả những gì bạn nói là nơi để tìm câu trả lời, không phải câu trả lời là gì .
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.