
"Christopher Kohlhoff" <chris@kohlhoff.com> wrote in message news:20051017052938.33920.qmail@web32604.mail.mud.yahoo.com...
Hi Beman,
--- Beman Dawes <bdawes@acm.org> wrote:
There are still some void *'s in the public interface. The io_control
helpers have data() members returning void *'s, for example.
Any chance of wringing out the non-memory management void *'s before a submission? I know it sounds picky, but a lot of C++ programmers object strongly to void * in public interfaces for anything except the rawest of raw memory management.
Hmm, I'm not sure. Here are the current uses of void* and their rationales:
* buffer() and buffers() - so that arbitrary application data structures can be sent without an additional buffer copy.
Where did this idea come from that the only way to avoid an additional copy is to use a void *? Think about the iterator interfaces to Standard Library algorithms. They traffic nicely in pointers, yet not a buffer copy or void * to be seen. So instead of: ... foo( void * data, size_t size); something like: template <class RandomAccessIterator> ... foo( RandomAccessIterator first, RandomAccessIterator last );
* const_buffer::data() and mutable_data::data() - to get a pointer to pass to OS functions like send and recv.
Return a pointer to the actual type. If the user ends up casting that to void* to use it with an old C interface, that's the user's choice. Or use cast-style syntax: template <class T> T data_cast(...);
* IO_Control_Command::data() - to get a pointer to pass to OS functions like ioctl or WSAIOCtl.
* Socket_Option::data() - to get a pointer to be pass to OS functions like setsocketopt and getsockopt.
Same comment as above.
The last 3 are similar, in that they involve getting a pointer to be passed to a low-level OS function. (Note that the Endpoint concept has a similar thing, except that it returns an implementation-defined type rather than void*.)
Have you considered defining one or more concepts for the low-level OS functions, and including a trivial wrapper to supply a model of the concept for the most common implementation of the OS functions?
I don't want to preclude user-defined IO_Control_Command or Socket_Option types, so there has to be *some* way of getting a pointer to the data in the public interface. However I'd be very happy to change it if there's a better way of accomplishing the same thing.
There isn't anything wrong with functions that return pointers (or more generally, iterators). The objection is to void * pointers breaking type safety. If the actual type is known (or even implementation-defined) then use it. If the actual type can't be known ahead of usage, supply the type as a template parameter. The above is very mom-and-apple-pie, so I expect you have already gone over it in your mind. And there are better interface designers than me reading this list, so others may have better ideas. But there is certainly some way to get rid of the void *'s, and the interface will be better for doing so. All IMO, of course.
I also wonder if the interface could be thinned without reducing functionality. For example, could buffer() and buffers() be folded into one function, or at least overloads with the same name.
I had considered making the mutable_buffer/const_buffer classes also implement the Mutable_Buffers/Const_Buffers concepts, but i found it was confusing as to whether the begin()/end() functions applied to the underlying memory.
What about this for an idea:
- Remove the buffer() functions.
- Rename buffers() to buffer().
- Have a specialisation for const_buffer<1> that supports conversion to const_buffer.
- Have a specialisation for mutable_buffer<1> that supports conversion to mutable_buffer and const_buffer.
Then to send a single buffer you could write:
sock.write(buffer(data, size));
and still use the chaining to send multiple buffers:
sock.write(buffer(data1, size1)(data2, size2));
The conversion to the individual buffer classes should still let you write:
std::vector<const_buffer> bufs; bufs.push_back(buffer(data1, size1)); bufs.push_back(buffer(data2, size2)); sock.write(bufs);'
or:
const_buffers<2> bufs = { buffer(data1, size1), buffer(data2, size2) }; sock.write(bufs);
Likewise, could the 12 free read/write functions be reduced to 2 names (presumably read/write) or 4 names (presumably read, async_read, write, async_write) via folding and/or overloading?. Am I the only one who feels the large number of names makes the interface appear more complex than it really is?
No you're not, especially after I've had to type async_read_at_least_n many times ;)
Reducing to 4 names might be feasible. One thing I realised recently is that, in terms of behaviour, read() and read_n() are just special cases of read_at_least_n(). The same obviously applies to write*() and the async equivalents.
That's the kind of thought process I suggesting you apply. Since you know the use cases far better than I do, you should be the one to make the decision as to how to fold these special cases into one more general case. All I can do is encourage you to make the effort.
Could something be done by extending the Mutable_Buffers/Const_Buffers concepts to have a desired_minimum_transfer() member function? Bad name, I know, but it would return the minimum number of bytes to read or write from the list of buffers.
It would have to be optional so that containers like std::vector can still meet the concept requirements. It would probably have a default of 0, i.e. same semantics as current asio::read() or asio::write().
You could then write:
read(sock, bufs); // Same as existing read()
read(sock, all_of(bufs)); // Same as read_n()
read(sock, at_least(bufs, 42)); // Same as read_at_least_n().
I'm ready to take guidance here :)
I've become convinced that the STL half-open range is the best way to represent a sequence, rather than the address of first element and a count. Thus your three forms might look like: read( sock, first ); // read one only read( sock, first, last ); // read n = =last-first read( sock, first, min, end ); // read at least min-first, at most end-first I would probably eliminate the first overload unless the case of wanting to read only one is very, very common. Take the above with a grain of salt since I'm not familiar with the problem domain and only commented in the first place because void *'s are such a red flag, and because there seemed to be a lot of names for very similar functionality. --Beman