Just two ideas, it's too late to actually think ;-)

you can change your 'ignored' vesion to something more acceptable:

...
def c1
  @c1_tmp1 ||= ExpensiveObject.new
  @c1_tmp2 ||= ExpensiveObject.new
  tmp1, tmp2 = @c1_tmp1, @c2.tmp2
  ...
end

I was thinking of some kind of delegation, but it got too messy so I
abandoned it.
Another possibility is to implement a pool of objects directly into
ExpensiveObject or to create a layer above it.

i.e. something like (not tested, not thread-safe, etc.):

class ExtensiveObjectPool
   class << self
     def initialize
         @idle, @busy = [], []
     end

     def with(how_many)
         objs = allocate(how_many)
         yield *objs
     ensure
         release(objs)
     end

     def allocate(num)
          ret = []
          while num > 0 and not @free.empty?
              num -= 1
              ret << @free.pop
              @busy << ret.last
          end
          while num > 0
              num -= 1
              ret << ExpensiveObject.new
              @busy << ret.last
          end
          ret
     end
     def release(objs)
         @busy.delete(*objs)
         @free += objs
     end
end

class Foo
   def c1
       ExtensiveObjectPool.with(2) do |tmp1, tmp2|
          result = tmp1 + tmp2 + c2
       end
   end

   def c2
       ExtensiveObjectPool.with(2) do |tmp1, tmp2|
          result = tmp1 + tmp2
       end
   end
end