Code Sfumato
sfumato, (from Italian sfumare, “to tone down” or “to evaporate like smoke”), in painting or drawing, the fine shading that produces soft, imperceptible transitions between colours and tones. It is used most often in connection with the work of Leonardo da Vinci and his followers, who made subtle gradations, without lines or borders, from light to dark areas; the technique was used for a highly illusionistic rendering of facial features and for atmospheric effects - Britannica Importantly for our purposes this technique was executed by the application of many, thin, translucent layers. Leonardo used paint but I believe the technique applies equally well to the art of software. This might also be thought of as, 'The power of leaving things out', 'Applied ignorance', 'No code is the best code' or just another positive form of laziness. In software development laziness has long been known to be a good thing. So long it has it's rules. Don't put off till tomorrow what you can put off till next week. If at first you don't succeed, get someone else to do it for you. Cheat at every opportunity. The rules of laziness are, of course, terrible advice for life but very good advice for writing code. That's it. That's what I wanted to say. Code Sfumato is the way I write my best code. I guess you want an example though ;= write_begin. We'll obviously need the analogous function for reading so we might as well add that in. size_t Buffer::ReadAcknowledge(size_t& itemCount) { if(itemCount > (m_readEnd - m_readBegin)) { itemCount = static_cast(m_readEnd - m_readBegin); } m_readBegin += itemCount; m_readEnd = m_readBegin; return ReadCapacity(); } This allows the client to tell us they're done reading itemCount items. However they might just want to say they are rejecting the last itemCount items they previously requested to read. They'll still need to acknowledge the ones they do read to move the read_begin point forward. size_t Buffer::ReadReject(size_t& itemCount) { if(itemCount > (m_readEnd - m_readBegin)) { itemCount = static_cast(m_readEnd - m_readBegin); } m_readEnd -= itemCount; return ReadCapacity(); } We're almost there I promise. Just two more functions on this class to go. All that's left is functions for the client to Request to Read, i.e. move the read_end point forward and to Request to Write, i.e. move the write_end point forward. virtual byte* Buffer::WriteRequest(size_t& /*itemCount*/) { return nullptr; } virtual byte* Buffer::ReadRequest(size_t& /*itemCount*/) { return nullptr; } What happened? Well we can't actually write these functions because they need to return actual pointers to data. We could complicate the Buffer class by adding real memory handling but that wouldn't be sfumato. Instead having gradually built up one layer and reached the point where the client facing interface is defined if not entirely implemented, we'll begin another layer. Layer 2: All the pointer magic in one place We'll use traditional Object Oriented extension by inheritance. A PODBuffer< pod_t > is a Buffer. We'll add a template parameter as still nobody has told us what goes in this Buffer. template< class pod_t > class PODBuffer : public Buffer { protected: pod_t* m_pAllocation{nullptr}; }; Let's add some standard class furniture and then deal with memory management template< class pod_t > class PODBuffer : public Buffer { public: PODBuffer(size_t itemCount = 0) : Buffer(sizeof(pod_t), itemCount) { SetCapacity(itemCount); } PODBuffer(const PODBuffer& src) { *this = src; } virtual ~PODBuffer() { //This is needed. Remind me to write an article //about why virtual functions don't work //in base class destructors. SetCapacity((size_t)0); } PODBuffer& operator = (const PODBuffer& src) { if(&src != this) { Buffer::operator = (src); memcpy( m_pAllocation, src.m_pAllocation, sizeof(pod_t) * std::min( m_allocationCount, src.m_allocationCount ) ); } return *this; } protected: pod_t* m_pAllocation{nullptr}; }; When the pod_t type is determined it's size will be available to become the Buffer unit size. We could add a requires clause using type traits to ensure it's a fixed size type however std::is_pod is deprecated and I'm not sure which one to use in its place. std::is_standard_layout ? Let me know and I'll add it. When we copy a PODBuffer, now we actually copy the contents as well. Taking care to use only the size of the smaller buffer. However so far there are no contents. Lets fix that by overriding SetCapacity virtual void SetCapacity(size_t itemCount) { if(m_allocationCount == itemCount && m_pAllocation != nullptr) { return; } delete[] m_pAllocation; m_pAllocation = (itemCount > 0) ? new pod_t[itemCount] : nullptr; memset(m_pAllocation, 0, sizeof(pod_t) * itemCount); Buffer::SetCapacity(itemCount); } That's it. One new and one delete. Now those SetCapacity((size_t)0) calls we did earlier don't look so silly. For added security we fill the Buffer with zeros. Let's override the failed implementation of WriteRequest now with one that returns a valid pointer. virtual byte* WriteRequest(size_t& itemCount) { pod_t* result = 0; if (itemCount == 0 || itemCount > WriteCapacity()) { itemCount = WriteCapacity(); } if (itemCount > 0) { result = AddressOf(m_writeBegin); m_writeEnd += itemCount;//write_end moves here } return reinterpret_cast(result); } It would be nice to return a pod_t* here but C++ doesn't do covariant return types so to override the truly ignorant Buffer::WriteRequest we have to return a byte*. The client can safely cast it back as they know what a pod_t is. Note that a write request is just that. Requesting to write 65 items might get you space to write 5 or 0 if the buffer is full. the client will need to take note of the value of itemCount after the call. This is how many items they are allowed to write. Requesting to read is very similar but first lets look at AddressOf pod_t* AddressOf(size_t index) { pod_t* result = nullptr; if (m_allocationCount > 0) { result = m_pAllocation + (index % m_allocationCount); } return result; } This gives an offset into the allocated space in multiples of sizeof(pod_t) which is always within the allocated space. Remember the + operator here is using pointer arithmetic rules. As m_pAllocation is a pod_t* so it adds in sizeof(pod_t*) sized units. Exactly what we want. ReadRequest uses the same function: virtual byte* ReadRequest(size_t& itemCount) { pod_t* result = AddressOf(m_readBegin); if (itemCount > ReadCapacity()) { itemCount = static_cast(ReadCapacity()); } m_readEnd += itemCount;//read_end moves here return reinterpret_cast(result); } You may have wondered how we keep the read_begin, read_end, write_begin and write_end values within the allocated space. The answer is that we don't. We care that Read follows Write and that End is >= Begin but we don't care what the actual numeric values are. (index % m_allocationCount) gives the correct position in the buffer and limiting the Write Capacity to the allocation size is sufficient to ensure that write_end is never more than allocation count units ahead of read_begin so we don't don't tread on our own tail as the output of (index % m_allocationCount) wraps around. This leaves us with just one problem. When we report Read and Write capacities to the client we don't take that wrap around into account. If the available space is split over the end of the buffer we can't give the client a contiguous block of memory to Read or Write. We could give them two but that would be a really weird interface. Here's most of the memory you asked for and here's the other bit which is usually null. Instead we'll just behave as if the second part wasn't available. To do this we need to override WriteCapacity and ReadCapacity virtual size_t WriteCapacity() { size_t result = m_allocationCount - static_cast(m_writeEnd - m_readBegin); if(AddressOf(m_writeBegin) + result > EndOfBuffer()) { result = EndOfBuffer() - AddressOf(m_writeBegin); } return result; } virtual size_t ReadCapacity() { size_t result = static_cast(m_writeBegin - m_readEnd); if(AddressOf(m_readEnd) + result > EndOfBuffer()) { result = EndOfBuffer() - AddressOf(m_readEnd); } return result; } pod_t* EndOfBuffer(void) { pod_t* result = nullptr; if (m_allocationCount > 0) { result = m_pAllocation + m_allocationCount; } return result; } That's really it this time. Here's the whole PODBuffer< pod_t > class along with streaming >> operators class PODBuffer : public Buffer { public: PODBuffer(size_t itemCount = 0) : Buffer(sizeof(pod_t), itemCount), m_pAllocation(nullptr) { SetCapacity(itemCount); } PODBuffer(const PODBuffer& src) { *this = src; } virtual ~PODBuffer() { SetCapacity((size_t)0); } PODBuffer& operator = (const PODBuffer& src) { if(&src != this) { Buffer::operator = (src); memcpy(m_pAllocation, src.m_pAllocation, sizeof(pod_t) * std::min( m_allocationCount, src.m_allocationCount ) ); } return *this; } PODBuffer& operator > (pod_t& item) { size_t count = 1; pod_t* pRead = reinterpret_cast (ReadRequest(count)); if(pRead && count == 1) { item = *pRead; ReadAcknowledge(count); } return *this; } virtual size_t WriteCapacity() { size_t result = m_allocationCount - static_cast(m_writeEnd - m_readBegin); if(AddressOf(m_writeBegin) + result > EndOfBuffer()) { result = EndOfBuffer() - AddressOf(m_writeBegin); } return result; } virtual size_t ReadCapacity() { size_t result = static_cast( m_writeBegin - m_readEnd); if(AddressOf(m_readEnd) + result > EndOfBuffer()) { result = EndOfBuffer() - AddressOf(m_readEnd); } return result; } virtual byte* WriteRequest(size_t& itemCount) { pod_t* pResult = 0; if (itemCount == 0 || itemCount > WriteCapacity()) { itemCount = WriteCapacity(); } if (itemCount > 0) { pResult = AddressOf(m_writeBegin); m_writeEnd += itemCount; } return reinterpret_cast(pResult); } virtual byte* ReadRequest(size_t& itemCount) { pod_t* pResult = AddressOf(m_readBegin); if (itemCount > ReadCapacity()) { itemCount = static_cast(ReadCapacity()); } m_readEnd += itemCount; return reinterpret_cast(pResult); } virtual void SetCapacity(size_t itemCount) { if(m_allocationCount == itemCount && m_pAllocation != nullptr) { return; } delete[] m_pAllocation; m_pAllocation = (itemCount > 0) ? new pod_t[itemCount] : nullptr; memset(m_pAllocation, 0, sizeof(pod_t) * itemCount); Buffer::SetCapacity(itemCount); } protected: pod_t* EndOfBuffer(void) { pod_t* pResult = nullptr; if (m_allocationCount > 0) { pResult = m_pAllocation + m_allocationCount; } return pResult; } pod_t* AddressOf(size_t index) { pod_t* result = nullptr; if (m_allocationCount > 0) { result = m_pAllocation + (index % m_allocationCount); } return result; } pod_t* m_pAllocation; }; and for completeness (because I'm not really very good at being lazy) typedef PODBuffer< byte > ByteBuffer; The two hard things that catch everyone out the first few times doing buffers are the memory management and preventing overruns. It turns out that actual memory management requires just two lines of code. delete[] m_pAllocation; m_pAllocation = (itemCount > 0) ? new pod_t[itemCount] : nullptr; And buffer overruns are terminated forever by just one. result = m_pAllocation + (index % m_allocationCount); We used the power of our ignorance of the real requirements to create a semi circular buffer class for any number of units of any fixed sized type. We left it up to the client to decide everything we didn't know and we delayed all the hard parts until they weren't hard or we didn't have to do them at all. We separated the task into layers that deal with different problems. Keeping every step as simple as possible. #include #include #include #include #include #include "podbuffer.h" struct test_s { bool b; const char* s; unsigned long long l; }; int main() { PODBuffer aBuffer(15); test_s test{ true, "something", 100000000 }; aBuffer > result; assert(memcmp(&result, &test, sizeof(test_s)) == 0); return 0; } Conclusion: What are the costs and benefits of this sfumato approach to writing code? The resulting code is certainly not compact. Although that doesn't matter as much in a compiled language like C++ as it would in Python or JS. You can end up with many layers and it requires a well organised source tree not to loose track of them. The performance has not been honed to the last clock cycle and cache line. I generally prefer to leave that to the optimising compiler which knows a great deal more about my hardware than I do. Is this buffer fast though? In practice you can count the nano seconds because they seldom reach 3 digits. Yes it's fast although you're welcome to make it faster and let me know how. The functions are small and simple. You may have noticed that there's no single function in this article that's more than a handful of lines. This makes writing unit tests as you go along easy. This also makes tracking down bugs and generally understanding the code you wrote 5 years ago, easier. You get reliable, readable, intentional, testable code with strong separation of concerns. So don't be afraid to write incomplete classes that only do part of the job. As long as the part they do is simple, safe and reliable. You can always add another layer of sfumato to solve the next problem. Abstracting each problem, or set of problems, into it's own thin layer leads to surprisingly pleasing results. This PODBuffer< pod_t > class, or one very like it, is in use in the general data pipeline library of the QuerySoft Open Runtime. I wrote the first version of it more than a decade ago and it's changed very little. (That's why some of the code style is a bit out of date) Do you have a favourite class or code snippet that you've been using for years across projects. Is it sfumato code? If you do please consider sharing it. Here's a snippet of Buffer in real use. It was streaming JSON from a file to a parser at the time but you can't tell by looking. bool Source::Pull(size_t& unitsRead, size_t unitsToRead) { if(!unitsToRead) { return true; } Buffer* buffer = GetBuffer(); if(buffer) { byte* space = buffer->WriteRequest(unitsToRead); if(!space) { log::debug("Buffer capacity: {0}, Write capacity: {1}, Read capacity: {2}, Unit size: {3}", buffer->Capacity(), buffer->WriteCapacity(), buffer->ReadCapacity(), buffer->GetUnitSize()); continuable("Pipeline stall. No space in source buffer."); return false; } size_t bytesRead = ReadBytes( space, buffer->GetUnitSize() * unitsToRead); if(bytesRead > 0) { unitsRead = bytesRead / buffer->GetUnitSize(); buffer->WriteAcknowledge(unitsRead); OnReadSuccess(unitsRead); } else { OnEndOfData(); } return true; } else { return false; } }
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to